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 "COscillatorDisplay.h"
18 #include "Oscillator.h"
19 #include <time.h>
20 #include "unitconversion.h"
21 #include "UserInteractions.h"
22 #include "guihelpers.h"
23 #include "SkinColors.h"
24 #include "RuntimeFont.h"
25 
26 #include "filesystem/import.h"
27 #include "guihelpers.h"
28 #include "CursorControlGuard.h"
29 #include <utility>
30 
31 /*
32  * Custom Editor Types
33  */
34 #include "AliasOscillator.h"
35 
36 using namespace VSTGUI;
37 
38 const float disp_pitch = 90.15f - 48.f;
39 const int wtbheight = 12;
40 
41 extern CFontRef displayFont;
42 
draw(CDrawContext * dc)43 void COscillatorDisplay::draw(CDrawContext *dc)
44 {
45     if (customEditor && !canHaveCustomEditor())
46     {
47         closeCustomEditor();
48     }
49 
50     if (customEditor && customEditorActive)
51     {
52         customEditor->draw(dc);
53         drawExtraEditButton(dc, "CLOSE");
54         return;
55     }
56 
57     pdata tp[2][n_scene_params]; // 0 is orange, 1 is blue
58     Oscillator *osces[2];
59     std::string olabel;
60     for (int c = 0; c < 2; ++c)
61     {
62         osces[c] = nullptr;
63 
64 #if OSC_MOD_ANIMATION
65         if (!is_mod && c > 0)
66             continue;
67 #else
68         if (c > 0)
69             continue;
70 #endif
71 
72         tp[c][oscdata->pitch.param_id_in_scene].f = 0;
73         for (int i = 0; i < n_osc_params; i++)
74             tp[c][oscdata->p[i].param_id_in_scene].i = oscdata->p[i].val.i;
75 
76 #if OSC_MOD_ANIMATION
77         if (c == 1)
78         {
79             auto modOut = 1.0;
80             auto activeScene = storage->getPatch().scene_active.val.i;
81             auto *scene = &(storage->getPatch().scene[activeScene]);
82             auto iter = scene->modulation_voice.begin();
83 
84             // You'd think you could use this right? But you can't. These are NULL if you don't have
85             // a voice except scene ones ModulationSource *ms = scene->modsources[modsource];
86 
87             bool isUnipolar = true;
88             double smt = 1.0 - (mod_time - (int)mod_time); // 1hz ramp
89 
90             std::ostringstream oss;
91             oss << modsource_names[modsource];
92             olabel = oss.str();
93 
94             if (modsource >= ms_lfo1 && modsource <= ms_slfo6)
95             {
96                 auto *lfo = &(scene->lfo[modsource - ms_lfo1]);
97 
98                 float frate = lfo->rate.val.f;
99                 if (lfo->rate.temposync)
100                     frate *= storage->temposyncratio;
101 
102                 auto freq = powf(2.0f, frate);
103 
104                 smt = sin(M_PI * freq * mod_time);
105                 if (lfo->shape.val.i == lt_square)
106                     smt = (smt > 0 ? 1 : -1);
107 
108                 if (lfo->unipolar.val.i)
109                 {
110                     smt = 0.5 * (smt + 1);
111                 }
112             }
113 
114             while (iter != scene->modulation_voice.end())
115             {
116                 int src_id = iter->source_id;
117                 if (src_id == modsource)
118                 {
119                     int dst_id = iter->destination_id;
120                     float depth = iter->depth;
121                     tp[c][dst_id].f += depth * modOut * smt;
122                 }
123                 iter++;
124             }
125 
126             iter = scene->modulation_scene.begin();
127 
128             while (iter != scene->modulation_scene.end())
129             {
130                 int src_id = iter->source_id;
131                 if (src_id == modsource)
132                 {
133                     int dst_id = iter->destination_id;
134                     float depth = iter->depth;
135                     tp[c][dst_id].f += depth * modOut * smt;
136                 }
137                 iter++;
138             }
139         }
140 #endif
141 
142         osces[c] = spawn_osc(oscdata->type.val.i, storage, oscdata, tp[c]);
143     }
144 
145     float h = getHeight();
146     float extraYTranslate = 0;
147     if (uses_wavetabledata(oscdata->type.val.i))
148     {
149         h -= wtbheight + 2.5;
150         extraYTranslate = 0.1 * wtbheight;
151     }
152 
153     int totalSamples = (1 << 4) * (int)getWidth();
154     int averagingWindow = 4; // this must be both less than BLOCK_SIZE_OS and BLOCK_SIZE_OS must be
155                              // an integer multiple of it
156 
157 #if LINUX && !TARGET_JUCE_UI
158     Surge::UI::NonIntegralAntiAliasGuard naag(dc);
159 #endif
160 
161     float valScale = 100.0f;
162 
163     auto size = getViewSize();
164 
165     for (int c = 1; c >= 0; --c) // backwards so we draw blue first
166     {
167         bool use_display = false;
168         Oscillator *osc = osces[c];
169         CGraphicsPath *path = dc->createGraphicsPath();
170         if (osc)
171         {
172             float disp_pitch_rs = disp_pitch + 12.0 * log2(dsamplerate / 44100.0);
173             if (!storage->isStandardTuning)
174             {
175                 // OK so in this case we need to find a better version of the note which gets us
176                 // that pitch. Sigh.
177                 auto pit = storage->note_to_pitch_ignoring_tuning(disp_pitch_rs);
178                 int bracket = -1;
179                 for (int i = 0; i < 128; ++i)
180                 {
181                     if (storage->note_to_pitch(i) < pit && storage->note_to_pitch(i + 1) > pit)
182                     {
183                         bracket = i;
184                         break;
185                     }
186                 }
187                 if (bracket >= 0)
188                 {
189                     float f1 = storage->note_to_pitch(bracket);
190                     float f2 = storage->note_to_pitch(bracket + 1);
191                     float frac = (pit - f1) / (f2 - f1);
192                     disp_pitch_rs = bracket + frac;
193                 }
194                 else
195                 {
196                     // What the hell type of scale is this folks! But this is just UI code so just
197                     // punt
198                 }
199             }
200             use_display = osc->allow_display();
201 
202             // Mis-install check #2
203             if (uses_wavetabledata(oscdata->type.val.i) && storage->wt_list.size() == 0)
204                 use_display = false;
205 
206             if (use_display)
207                 osc->init(disp_pitch_rs, true, true);
208 
209             int block_pos = BLOCK_SIZE_OS;
210             for (int i = 0; i < totalSamples; i += averagingWindow)
211             {
212                 if (use_display && (block_pos >= BLOCK_SIZE_OS))
213                 {
214                     if (uses_wavetabledata(oscdata->type.val.i))
215                     {
216                         storage->waveTableDataMutex.lock();
217                         osc->process_block(disp_pitch_rs);
218                         block_pos = 0;
219                         storage->waveTableDataMutex.unlock();
220                     }
221                     else
222                     {
223                         osc->process_block(disp_pitch_rs);
224                         block_pos = 0;
225                     }
226                 }
227 
228                 float val = 0.f;
229                 if (use_display)
230                 {
231                     for (int j = 0; j < averagingWindow; ++j)
232                     {
233                         val += osc->output[block_pos];
234                         block_pos++;
235                     }
236                     val = val / averagingWindow;
237                     val =
238                         ((-val + 1.0f) * 0.5f * (1.0 - scaleDownBy) + 0.5 * scaleDownBy) * valScale;
239                 }
240                 block_pos++;
241                 float xc = valScale * i / totalSamples;
242 
243                 // OK so val is now a value between 0 and valScale, and xc is a value between 0 and
244                 // valScale
245                 if (i == 0)
246                 {
247                     path->beginSubpath(xc, val);
248                 }
249                 else
250                 {
251                     path->addLine(xc, val);
252                 }
253             }
254             // srand( (unsigned)time( NULL ) );
255         }
256         // OK so now we need to figure out how to transfer the box with is [0,valscale] x
257         // [0,valscale] to our coords. So scale then position
258         VSTGUI::CGraphicsTransform tf = VSTGUI::CGraphicsTransform()
259                                             .scale(getWidth() / valScale, h / valScale)
260                                             .translate(size.getTopLeft().x, size.getTopLeft().y)
261                                             .translate(0, extraYTranslate);
262 #if LINUX && !TARGET_JUCE_UI
263         auto tmps = size;
264         dc->getCurrentTransform().transform(tmps);
265         VSTGUI::CGraphicsTransform tpath = VSTGUI::CGraphicsTransform()
266                                                .scale(getWidth() / valScale, h / valScale)
267                                                .translate(tmps.getTopLeft().x, tmps.getTopLeft().y);
268 #else
269         auto tpath = tf;
270 #endif
271 
272         dc->saveGlobalState();
273 
274         CRect waveBoundsRect;
275         if (c == 0)
276         {
277             float linesAreInBy = scaleDownBy * 0.5;
278             // OK so draw the rules
279             CPoint mid0(0.f, valScale / 2.f), mid1(valScale, valScale / 2.f);
280             CPoint top0(0.f, valScale * (1.0 - linesAreInBy)),
281                 top1(valScale, valScale * (1.0 - linesAreInBy));
282             CPoint bot0(0.f, valScale * linesAreInBy), bot1(valScale, valScale * linesAreInBy);
283 
284             CPoint clip0(0.f, valScale * linesAreInBy),
285                 clip1(valScale, valScale * (1.0 - linesAreInBy));
286             tf.transform(mid0);
287 
288             tf.transform(mid1);
289             tf.transform(top0);
290             tf.transform(top1);
291             tf.transform(bot0);
292             tf.transform(bot1);
293             tf.transform(clip0);
294             tf.transform(clip1);
295 
296             waveBoundsRect =
297                 CRect(clip0.x, clip0.y - 0.5, clip1.x, clip1.y + 0.5); // allow draws on the line
298             dc->setDrawMode(VSTGUI::kAntiAliasing | VSTGUI::kNonIntegralMode);
299 
300             dc->setLineWidth(1.0);
301             dc->setFrameColor(skin->getColor(Colors::Osc::Display::Center));
302             dc->drawLine(mid0, mid1);
303 
304             dc->setLineWidth(1.0);
305             dc->setFrameColor(skin->getColor(Colors::Osc::Display::Bounds));
306             dc->drawLine(top0, top1);
307             dc->drawLine(bot0, bot1);
308 
309             // Now draw dots
310             auto pointColor = skin->getColor(Colors::Osc::Display::Dots);
311             int nxd = 21, nyd = 13;
312             for (int xd = 0; xd < nxd; xd++)
313             {
314                 float normx = 1.f * xd / (nxd - 1) * 0.99;
315                 for (int yd = 1; yd < nyd - 1; yd++)
316                 {
317                     if (yd == (nyd - 1) / 2)
318                         continue;
319 
320                     float normy = 1.f * yd / (nyd - 1);
321                     auto dotPoint = CPoint(normx * valScale, (0.8 * normy + 0.1) * valScale);
322                     tf.transform(dotPoint);
323                     float esize = 0.5;
324                     float xoff = (xd == 0 ? esize : 0);
325                     auto er = CRect(dotPoint.x - esize + xoff, dotPoint.y - esize,
326                                     dotPoint.x + esize + xoff, dotPoint.y + esize);
327 #if LINUX && !TARGET_JUCE_UI
328                     dc->drawPoint(dotPoint, pointColor);
329 #else
330                     dc->setFillColor(pointColor);
331                     dc->drawEllipse(er, VSTGUI::kDrawFilled);
332 #endif
333                 }
334             }
335 
336             // OK so now the label
337             if (osces[1])
338             {
339                 dc->setFontColor(skin->getColor(Colors::Osc::Display::AnimatedWave));
340                 dc->setFont(displayFont);
341                 CPoint lab0(0, valScale * 0.1 - 10);
342                 tf.transform(lab0);
343                 CRect rlab(lab0, CPoint(10, 10));
344                 dc->drawString(olabel.c_str(), rlab, kLeftText, true);
345             }
346         }
347 
348         dc->setLineWidth(1.3);
349 #if LINUX && !TARGET_JUCE_UI
350         dc->setDrawMode(VSTGUI::kAntiAliasing | VSTGUI::kNonIntegralMode);
351 #else
352         dc->setDrawMode(VSTGUI::kAntiAliasing);
353 #endif
354         if (c == 1)
355             dc->setFrameColor(VSTGUI::CColor(100, 100, 180, 0xFF));
356         else
357             dc->setFrameColor(skin->getColor(Colors::Osc::Display::Wave));
358 
359         if (use_display)
360         {
361             CRect oldcr;
362 #if TARGET_JUCE_UI
363             dc->setClipRect(waveBoundsRect);
364 #endif
365             dc->drawGraphicsPath(path, VSTGUI::CDrawContext::PathDrawMode::kPathStroked, &tpath);
366         }
367         dc->restoreGlobalState();
368 
369         path->forget();
370 
371         delete osces[c];
372     }
373 
374     if (uses_wavetabledata(oscdata->type.val.i))
375     {
376         CRect wtlbl(size);
377         wtlbl.top = wtlbl.bottom - wtbheight;
378         wtlbl.offset(0, -4);
379         rmenu = wtlbl;
380         rmenu.inset(14, 0);
381 
382         rprev = wtlbl;
383         rprev.right = rmenu.left; // -1;
384         rnext = wtlbl;
385         rnext.left = rmenu.right; // +1;
386 
387         char wttxt[256];
388 
389         storage->waveTableDataMutex.lock();
390 
391         int wtid = oscdata->wt.current_id;
392         if (oscdata->wavetable_display_name[0] != '\0')
393         {
394             strcpy(wttxt, oscdata->wavetable_display_name);
395         }
396         else if ((wtid >= 0) && (wtid < storage->wt_list.size()))
397         {
398             strcpy(wttxt, storage->wt_list.at(wtid).name.c_str());
399         }
400         else if (oscdata->wt.flags & wtf_is_sample)
401         {
402             strcpy(wttxt, "(Patch Sample)");
403         }
404         else
405         {
406             strcpy(wttxt, "(Patch Wavetable)");
407         }
408 
409         storage->waveTableDataMutex.unlock();
410 
411         char *r = strrchr(wttxt, '.');
412         if (r)
413             *r = 0;
414         auto fgcol = skin->getColor(Colors::Osc::Filename::Background);
415         auto fgframe = skin->getColor(Colors::Osc::Filename::Frame);
416         auto fgtext = skin->getColor(Colors::Osc::Filename::Text);
417 
418         auto fgcolHov = skin->getColor(Colors::Osc::Filename::BackgroundHover);
419         auto fgframeHov = skin->getColor(Colors::Osc::Filename::FrameHover);
420         auto fgtextHov = skin->getColor(Colors::Osc::Filename::TextHover);
421 
422         dc->setFillColor(fgcol);
423         auto rbg = rmenu;
424 
425         if (skin->getVersion() >= 2)
426         {
427             if (isWTHover == MENU)
428             {
429                 dc->setFrameColor(fgframeHov);
430                 dc->setFillColor(fgcolHov);
431             }
432             else
433             {
434                 dc->setFillColor(fgcol);
435                 dc->setFrameColor(fgframe);
436             }
437             dc->drawRect(rmenu, kDrawFilledAndStroked);
438         }
439         else
440         {
441             dc->setFillColor(fgcol);
442             dc->drawRect(rmenu, kDrawFilled);
443         }
444 
445         if (skin->getVersion() >= 2 && isWTHover == MENU)
446         {
447             dc->setFontColor(skin->getColor(Colors::Osc::Filename::TextHover));
448         }
449         else
450         {
451             dc->setFontColor(skin->getColor(Colors::Osc::Filename::Text));
452         }
453         dc->setFont(displayFont);
454         dc->drawString(wttxt, rmenu, kCenterText, true);
455 
456         /*CRect wtlbl_status(size);
457         wtlbl_status.bottom = wtlbl_status.top + wtbheight;
458         dc->setFontColor(kBlackCColor);
459         if(oscdata->wt.flags & wtf_is_sample) dc->drawString("IS
460         SAMPLE",wtlbl_status,false,kRightText);*/
461 
462         if (skin->getVersion() == 1)
463         {
464             dc->setFillColor(fgcol);
465             dc->drawRect(rprev, kDrawFilled);
466             dc->drawRect(rnext, kDrawFilled);
467         }
468         else
469         {
470             if (isWTHover == PREV)
471             {
472                 dc->setFrameColor(fgframeHov);
473                 dc->setFillColor(fgcolHov);
474             }
475             else
476             {
477                 dc->setFillColor(fgcol);
478                 dc->setFrameColor(fgframe);
479             }
480             dc->drawRect(rprev, kDrawFilledAndStroked);
481 
482             if (isWTHover == NEXT)
483             {
484                 dc->setFrameColor(fgframeHov);
485                 dc->setFillColor(fgcolHov);
486             }
487             else
488             {
489                 dc->setFillColor(fgcol);
490                 dc->setFrameColor(fgframe);
491             }
492             dc->drawRect(rnext, kDrawFilledAndStroked);
493         }
494         dc->setFrameColor(kBlackCColor);
495 
496         dc->saveGlobalState();
497 
498         dc->setDrawMode(kAntiAliasing);
499 
500         auto marginy = 2;
501         float triw = 6;
502         float trih = rprev.getHeight() - (marginy * 2);
503         float trianch = rprev.top + marginy;
504         float triprevstart = rprev.left + ((rprev.getWidth() - triw) / 2.f);
505         float trinextstart = rnext.right - ((rnext.getWidth() - triw) / 2.f);
506 
507         VSTGUI::CDrawContext::PointList trinext;
508         if (skin->getVersion() >= 2 && isWTHover == NEXT)
509         {
510             dc->setFillColor(skin->getColor(Colors::Osc::Filename::TextHover));
511         }
512         else
513         {
514             dc->setFillColor(skin->getColor(Colors::Osc::Filename::Text));
515         }
516         trinext.push_back(VSTGUI::CPoint(trinextstart - triw, trianch));
517         trinext.push_back(VSTGUI::CPoint(trinextstart, trianch + (trih / 2.f)));
518         trinext.push_back(VSTGUI::CPoint(trinextstart - triw, trianch + trih));
519         dc->drawPolygon(trinext, kDrawFilled);
520 
521         VSTGUI::CDrawContext::PointList triprev;
522         if (skin->getVersion() >= 2 && isWTHover == PREV)
523         {
524             dc->setFillColor(skin->getColor(Colors::Osc::Filename::TextHover));
525         }
526         else
527         {
528             dc->setFillColor(skin->getColor(Colors::Osc::Filename::Text));
529         }
530         triprev.push_back(VSTGUI::CPoint(triprevstart + triw, trianch));
531         triprev.push_back(VSTGUI::CPoint(triprevstart, trianch + (trih / 2.f)));
532         triprev.push_back(VSTGUI::CPoint(triprevstart + triw, trianch + trih));
533 
534         dc->drawPolygon(triprev, kDrawFilled);
535 
536         dc->restoreGlobalState();
537     }
538 
539     if (canHaveCustomEditor())
540     {
541         drawExtraEditButton(dc, "EDIT");
542     }
543 
544     setDirty(false);
545 }
546 
onMouseDown(CPoint & where,const CButtonState & button)547 CMouseEventResult COscillatorDisplay::onMouseDown(CPoint &where, const CButtonState &button)
548 {
549     if (canHaveCustomEditor() && customEditButtonRect.pointInside(where))
550     {
551         if (customEditorActive)
552         {
553             closeCustomEditor();
554         }
555         else
556         {
557             openCustomEditor();
558         }
559         invalid();
560         return kMouseDownEventHandledButDontNeedMovedOrUpEvents;
561     }
562 
563     if (customEditorActive && customEditor)
564     {
565         return customEditor->onMouseDown(where, button);
566     }
567 
568     if (listener && (button & (kMButton | kButton4 | kButton5)))
569     {
570         listener->controlModifierClicked(this, button);
571         return kMouseDownEventHandledButDontNeedMovedOrUpEvents;
572     }
573 
574     assert(oscdata);
575 
576     if (!uses_wavetabledata(oscdata->type.val.i) || (where.y < rmenu.top))
577     {
578         controlstate = 1;
579         lastpos.x = where.x;
580         return kMouseEventHandled;
581     }
582     else if (uses_wavetabledata(oscdata->type.val.i))
583     {
584         int id = oscdata->wt.current_id;
585         if (rprev.pointInside(where))
586         {
587             id = storage->getAdjacentWaveTable(oscdata->wt.current_id, false);
588             if (id >= 0)
589                 oscdata->wt.queue_id = id;
590         }
591         else if (rnext.pointInside(where))
592         {
593             id = storage->getAdjacentWaveTable(oscdata->wt.current_id, true);
594             if (id >= 0)
595                 oscdata->wt.queue_id = id;
596         }
597         else if (rmenu.pointInside(where))
598         {
599             CRect menurect(0, 0, 0, 0);
600             menurect.offset(where.x, where.y);
601             COptionMenu *contextMenu =
602                 new COptionMenu(menurect, 0, 0, 0, 0, COptionMenu::kMultipleCheckStyle);
603 
604             populateMenu(contextMenu, id);
605 
606             getFrame()->addView(contextMenu); // add to frame
607             contextMenu->setDirty();
608             contextMenu->popup();
609 #if !TARGET_JUCE_UI
610             // wth is this?
611             contextMenu->onMouseDown(where, kLButton); // <-- modal menu loop is here
612 #endif
613             // getFrame()->looseFocus(pContext);
614 
615             getFrame()->removeView(contextMenu, true); // remove from frame and forget
616         }
617     }
618     return kMouseDownEventHandledButDontNeedMovedOrUpEvents;
619 }
620 
populateMenu(COptionMenu * contextMenu,int selectedItem)621 void COscillatorDisplay::populateMenu(COptionMenu *contextMenu, int selectedItem)
622 {
623     int idx = 0;
624     bool needToAddSep = false;
625     for (auto c : storage->wtCategoryOrdering)
626     {
627         if (idx == storage->firstThirdPartyWTCategory ||
628             (idx == storage->firstUserWTCategory &&
629              storage->firstUserWTCategory != storage->wt_category.size()))
630         {
631             needToAddSep = true; // only add if there's actually data coming so defer the add
632         }
633 
634         idx++;
635 
636         PatchCategory cat = storage->wt_category[c];
637         if (cat.numberOfPatchesInCategoryAndChildren == 0)
638             continue;
639 
640         if (cat.isRoot)
641         {
642             if (needToAddSep)
643             {
644                 contextMenu->addEntry("-");
645                 needToAddSep = false;
646             }
647             populateMenuForCategory(contextMenu, c, selectedItem);
648         }
649     }
650 
651     // Add direct open here
652     contextMenu->addSeparator();
653 
654     auto renameItem = new CCommandMenuItem(
655         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Change Wavetable Display Name...")));
656     auto rnaction = [this](CCommandMenuItem *item) {
657         char c[256];
658         strncpy(c, this->oscdata->wavetable_display_name, 256);
659         auto *sge = dynamic_cast<SurgeGUIEditor *>(listener);
660         if (sge)
661         {
662             sge->promptForMiniEdit(
663                 c, "Enter a custom wavetable display name:", "Wavetable Display Name",
664                 CPoint(-1, -1), [this](const std::string &s) {
665                     strncpy(this->oscdata->wavetable_display_name, s.c_str(), 256);
666                     this->invalid();
667                 });
668         }
669     };
670     renameItem->setActions(rnaction, nullptr);
671     contextMenu->addEntry(renameItem);
672 
673     auto refreshItem = new CCommandMenuItem(
674         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Refresh Wavetable List")));
675     auto refresh = [this](CCommandMenuItem *item) { this->storage->refresh_wtlist(); };
676     refreshItem->setActions(refresh, nullptr);
677     contextMenu->addEntry(refreshItem);
678 
679     contextMenu->addSeparator();
680 
681     auto actionItem = new CCommandMenuItem(
682         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Load Wavetable from File...")));
683     auto action = [this](CCommandMenuItem *item) { this->loadWavetableFromFile(); };
684     actionItem->setActions(action, nullptr);
685     contextMenu->addEntry(actionItem);
686 
687     auto exportItem = new CCommandMenuItem(
688         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Save Wavetable to File...")));
689     auto exportAction = [this](CCommandMenuItem *item) {
690         int oscNum = this->osc_in_scene;
691         int scene = this->scene;
692 
693         std::string baseName = storage->getPatch().name + "_osc" + std::to_string(oscNum + 1) +
694                                "_scene" + (scene == 0 ? "A" : "B");
695         storage->export_wt_wav_portable(baseName, &(oscdata->wt));
696     };
697     exportItem->setActions(exportAction, nullptr);
698     contextMenu->addEntry(exportItem);
699 
700     auto omi = new CCommandMenuItem(
701         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Open Exported Wavetables Folder...")));
702     omi->setActions([this](CCommandMenuItem *i) {
703         Surge::UserInteractions::openFolderInFileBrowser(
704             Surge::Storage::appendDirectory(this->storage->userDataPath, "Exported Wavetables"));
705     });
706     contextMenu->addEntry(omi);
707 
708     auto *sge = dynamic_cast<SurgeGUIEditor *>(listener);
709     if (sge)
710     {
711         auto hu = sge->helpURLForSpecial("wavetables");
712         if (hu != "")
713         {
714             auto lurl = sge->fullyResolvedHelpURL(hu);
715             auto hi = new CCommandMenuItem(CCommandMenuItem::Desc("[?] Wavetables"));
716             auto ca = [lurl](CCommandMenuItem *i) { Surge::UserInteractions::openURL(lurl); };
717             hi->setActions(ca, nullptr);
718             contextMenu->addSeparator();
719             contextMenu->addEntry(hi);
720         }
721     }
722 }
723 
populateMenuForCategory(COptionMenu * contextMenu,int categoryId,int selectedItem)724 bool COscillatorDisplay::populateMenuForCategory(COptionMenu *contextMenu, int categoryId,
725                                                  int selectedItem)
726 {
727     char name[NAMECHARS];
728     COptionMenu *subMenu =
729         new COptionMenu(getViewSize(), 0, categoryId, 0, 0, COptionMenu::kMultipleCheckStyle);
730     subMenu->setNbItemsPerColumn(32);
731     int sub = 0;
732 
733     PatchCategory cat = storage->wt_category[categoryId];
734 
735     for (auto p : storage->wtOrdering)
736     {
737         if (storage->wt_list[p].category == categoryId)
738         {
739             sprintf(name, "%s", storage->wt_list[p].name.c_str());
740             auto actionItem = new CCommandMenuItem(CCommandMenuItem::Desc(name));
741             auto action = [this, p](CCommandMenuItem *item) { this->loadWavetable(p); };
742 
743             if (p == selectedItem)
744                 actionItem->setChecked(true);
745             actionItem->setActions(action, nullptr);
746             subMenu->addEntry(actionItem);
747 
748             sub++;
749         }
750     }
751 
752     bool selected = false;
753 
754     for (auto child : cat.children)
755     {
756         if (child.numberOfPatchesInCategoryAndChildren > 0)
757         {
758             // this isn't the best approach but it works
759             int cidx = 0;
760             for (auto &cc : storage->wt_category)
761             {
762                 if (cc.name == child.name)
763                     break;
764                 cidx++;
765             }
766 
767             bool subSel = populateMenuForCategory(subMenu, cidx, selectedItem);
768             selected = selected || subSel;
769         }
770     }
771 
772     if (!cat.isRoot)
773     {
774         std::string catName = storage->wt_category[categoryId].name;
775         std::size_t sepPos = catName.find_last_of(PATH_SEPARATOR);
776         if (sepPos != std::string::npos)
777         {
778             catName = catName.substr(sepPos + 1);
779         }
780         strncpy(name, catName.c_str(), NAMECHARS);
781     }
782     else
783     {
784         strncpy(name, storage->wt_category[categoryId].name.c_str(), NAMECHARS);
785     }
786 
787     CMenuItem *submenuItem = contextMenu->addEntry(subMenu, name);
788 
789     if (selected || (selectedItem >= 0 && storage->wt_list[selectedItem].category == categoryId))
790     {
791         selected = true;
792         submenuItem->setChecked(true);
793     }
794 
795     subMenu->forget(); // Important, so that the refcounter gets right
796     return selected;
797 }
798 
loadWavetable(int id)799 void COscillatorDisplay::loadWavetable(int id)
800 {
801     if (id >= 0 && (id < storage->wt_list.size()))
802     {
803         oscdata->wt.queue_id = id;
804     }
805 }
806 
loadWavetableFromFile()807 void COscillatorDisplay::loadWavetableFromFile()
808 {
809     Surge::UserInteractions::promptFileOpenDialog("", "", "", [this](std::string s) {
810         strncpy(this->oscdata->wt.queue_filename, s.c_str(), 255);
811     });
812 }
813 
onMouseUp(CPoint & where,const CButtonState & buttons)814 CMouseEventResult COscillatorDisplay::onMouseUp(CPoint &where, const CButtonState &buttons)
815 {
816     if (customEditorActive && customEditor)
817     {
818         return customEditor->onMouseUp(where, buttons);
819     }
820     if (controlstate)
821     {
822         controlstate = 0;
823     }
824     return kMouseEventHandled;
825 }
onMouseMoved(CPoint & where,const CButtonState & buttons)826 CMouseEventResult COscillatorDisplay::onMouseMoved(CPoint &where, const CButtonState &buttons)
827 {
828     bool newEditButton = false;
829 
830     if (canHaveCustomEditor() && customEditButtonRect.pointInside(where))
831     {
832         newEditButton = true;
833     }
834 
835     if (newEditButton != editButtonHover)
836     {
837         editButtonHover = newEditButton;
838         invalid();
839     }
840 
841     if (customEditorActive && customEditor)
842     {
843         return customEditor->onMouseMoved(where, buttons);
844     }
845 
846     if (uses_wavetabledata(oscdata->type.val.i))
847     {
848         auto owt = isWTHover;
849         isWTHover = NONE;
850         if (uses_wavetabledata(oscdata->type.val.i))
851         {
852             if (rprev.pointInside(where))
853             {
854                 isWTHover = PREV;
855             }
856             else if (rnext.pointInside(where))
857             {
858                 isWTHover = NEXT;
859             }
860             else if (rmenu.pointInside(where))
861             {
862                 isWTHover = MENU;
863             }
864         }
865         if (owt != isWTHover)
866             invalid();
867     }
868     else
869     {
870         if (isWTHover != NONE)
871         {
872             isWTHover = NONE;
873             invalid();
874         }
875         // getFrame()->setCursor( VSTGUI::kCursorDefault );
876     }
877 
878     if (controlstate)
879     {
880         /*oscdata->startphase.val.f -= 0.005f * (where.x - lastpos.x);
881         oscdata->startphase.val.f = limit_range(oscdata->startphase.val.f,0.f,1.f);
882         lastpos.x = where.x;
883         invalid();*/
884     }
885     return kMouseEventHandled;
886 }
887 
onWheel(const VSTGUI::CPoint & where,const float & distance,const VSTGUI::CButtonState & buttons)888 bool COscillatorDisplay::onWheel(const VSTGUI::CPoint &where, const float &distance,
889                                  const VSTGUI::CButtonState &buttons)
890 {
891     if (customEditorActive && customEditor)
892     {
893         return customEditor->onWheel(where, distance, buttons);
894     }
895     return false;
896 }
897 
invalidateIfIdIsInRange(int id)898 void COscillatorDisplay::invalidateIfIdIsInRange(int id)
899 {
900     auto *currOsc = &oscdata->type;
901     auto *endOsc = &oscdata->retrigger;
902     bool oscInvalid = false;
903     while (currOsc <= endOsc && !oscInvalid)
904     {
905         if (currOsc->id == id)
906             oscInvalid = true;
907         currOsc++;
908     }
909 
910     if (oscInvalid)
911     {
912         invalid();
913     }
914 }
onMouseEntered(CPoint & where,const CButtonState & buttons)915 CMouseEventResult COscillatorDisplay::onMouseEntered(CPoint &where, const CButtonState &buttons)
916 {
917     isWTHover = NONE;
918     if (uses_wavetabledata(oscdata->type.val.i))
919     {
920         if (rprev.pointInside(where))
921         {
922             isWTHover = PREV;
923         }
924         else if (rnext.pointInside(where))
925         {
926             isWTHover = NEXT;
927         }
928         else if (rmenu.pointInside(where))
929         {
930             isWTHover = MENU;
931         }
932     }
933 
934     if (customEditorActive && customEditor)
935         customEditor->onMouseEntered(where, buttons);
936 
937     invalid();
938     return kMouseEventHandled;
939 }
onMouseExited(CPoint & where,const CButtonState & buttons)940 CMouseEventResult COscillatorDisplay::onMouseExited(CPoint &where, const CButtonState &buttons)
941 {
942     if (canHaveCustomEditor() && !customEditButtonRect.pointInside(where))
943     {
944         if (editButtonHover)
945         {
946             editButtonHover = false;
947             invalid();
948         }
949     }
950 
951     isWTHover = NONE;
952     invalid();
953     if (customEditorActive && customEditor)
954         customEditor->onMouseExited(where, buttons);
955     return kMouseEventHandled;
956 }
957 
958 /*
959  * Custom Editor Support
960  */
canHaveCustomEditor()961 bool COscillatorDisplay::canHaveCustomEditor()
962 {
963     if (oscdata->type.val.i == ot_alias &&
964         oscdata->p[AliasOscillator::ao_wave].val.i == AliasOscillator::aow_additive)
965     {
966         return true;
967     }
968 
969     return false;
970 }
971 
openCustomEditor()972 void COscillatorDisplay::openCustomEditor()
973 {
974     customEditorActive = false;
975     if (!canHaveCustomEditor())
976         return; // shouldn't happen but hey...
977 
978     struct AliasAdditive : public CustomEditor
979     {
980         explicit AliasAdditive(COscillatorDisplay *d) : CustomEditor(d) {}
981         std::array<CRect, AliasOscillator::n_additive_partials> sliders;
982 
983         void draw(VSTGUI::CDrawContext *dc) override
984         {
985             auto vs = disp->getViewSize();
986             auto divSize = vs;
987             divSize.top += 12;
988             divSize.bottom -= 11;
989             auto w = divSize.getWidth() / AliasOscillator::n_additive_partials;
990 
991             dc->saveGlobalState();
992             dc->setDrawMode(VSTGUI::kAntiAliasing | VSTGUI::kNonIntegralMode);
993 
994             for (int i = 0; i < AliasOscillator::n_additive_partials; ++i)
995             {
996                 auto v = limit_range(disp->oscdata->extraConfig.data[i], -1.f, 1.f);
997                 auto p = CRect(CPoint(divSize.left + i * w, divSize.top),
998                                CPoint(w, divSize.getHeight()));
999 
1000                 sliders[i] = p;
1001 
1002                 auto q = p;
1003                 q.top = q.bottom - p.getHeight() / 2 * (1 + v);
1004                 q.bottom = q.bottom - p.getHeight() / 2;
1005 
1006                 if (q.top < q.bottom)
1007                 {
1008                     std::swap(q.bottom, q.top);
1009                 }
1010 
1011                 dc->setFillColor(disp->skin->getColor(Colors::Osc::Display::Wave));
1012                 dc->drawRect(q, kDrawFilled);
1013 
1014                 dc->setFrameColor(disp->skin->getColor(Colors::Osc::Display::Bounds));
1015 
1016                 if (i != 0)
1017                 {
1018                     dc->drawLine(p.getTopLeft(), p.getBottomLeft().offset(0, -1));
1019                 }
1020 
1021                 /*
1022                  * For now don't draw hovers
1023                 if (i == hoveredSegment)
1024                 {
1025                     // FIXME probably not this color
1026                     dc->setFillColor(disp->skin->getColor(Colors::Osc::Filename::BackgroundHover));
1027                     dc->drawRect(q, kDrawFilled);
1028                 }
1029                  */
1030             }
1031 
1032             auto midpoint = divSize.top + (divSize.getHeight() / 2);
1033             auto midl = CPoint(divSize.left, midpoint);
1034             auto midr = CPoint(divSize.right, midpoint);
1035 
1036             dc->drawLine(midl, midr);
1037 
1038             dc->setFrameColor(disp->skin->getColor(Colors::Osc::Display::Center));
1039             dc->drawRect(divSize, kDrawStroked);
1040 
1041             dc->restoreGlobalState();
1042         }
1043 
1044         CMouseEventResult onMouseDown(CPoint &where, const CButtonState &buttons) override
1045         {
1046             dragMode = NONE;
1047 
1048             if (buttons & (kDoubleClick | kControl))
1049             {
1050                 int i = 0;
1051 
1052                 for (auto &s : sliders)
1053                 {
1054                     if (s.pointInside(where))
1055                     {
1056                         disp->oscdata->extraConfig.data[i] = 0;
1057                         disp->invalid();
1058                     }
1059                     i++;
1060                 }
1061             }
1062 
1063             if (buttons & kRButton)
1064             {
1065                 auto contextMenu = new COptionMenu(CRect(where, CPoint(0, 0)), 0, 0, 0, 0,
1066                                                    COptionMenu::kMultipleCheckStyle);
1067 
1068                 {
1069                     auto actionItem = new CCommandMenuItem(
1070                         CCommandMenuItem::Desc("[?] Alias Osc Additive Options"));
1071                     contextMenu->addEntry(actionItem);
1072                     contextMenu->addSeparator();
1073                 }
1074                 {
1075                     auto actionItem = new CCommandMenuItem(
1076                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Sine")));
1077                     auto action = [this](CCommandMenuItem *item) {
1078                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1079                         {
1080                             disp->oscdata->extraConfig.data[qq] = (qq == 0) ? 1 : 0;
1081                         }
1082                     };
1083                     actionItem->setActions(action, nullptr);
1084                     contextMenu->addEntry(actionItem);
1085                 }
1086                 {
1087                     auto actionItem = new CCommandMenuItem(
1088                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Triangle")));
1089                     auto action = [this](CCommandMenuItem *item) {
1090                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1091                         {
1092                             disp->oscdata->extraConfig.data[qq] =
1093                                 (qq % 2 == 0) * 1.f / ((qq + 1) * (qq + 1));
1094                             if (qq % 4 == 2)
1095                                 disp->oscdata->extraConfig.data[qq] *= -1.f;
1096                         }
1097                     };
1098                     actionItem->setActions(action, nullptr);
1099                     contextMenu->addEntry(actionItem);
1100                 }
1101                 {
1102                     auto actionItem = new CCommandMenuItem(
1103                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Sawtooth")));
1104                     auto action = [this](CCommandMenuItem *item) {
1105                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1106                         {
1107                             disp->oscdata->extraConfig.data[qq] = 1.f / (qq + 1);
1108                         }
1109                     };
1110                     actionItem->setActions(action, nullptr);
1111                     contextMenu->addEntry(actionItem);
1112                 }
1113                 {
1114                     auto actionItem = new CCommandMenuItem(
1115                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Square")));
1116                     auto action = [this](CCommandMenuItem *item) {
1117                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1118                         {
1119                             disp->oscdata->extraConfig.data[qq] = (qq % 2 == 0) * 1.f / (qq + 1);
1120                         }
1121                     };
1122                     actionItem->setActions(action, nullptr);
1123                     contextMenu->addEntry(actionItem);
1124                 }
1125                 {
1126                     auto actionItem = new CCommandMenuItem(
1127                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Random")));
1128                     auto action = [this](CCommandMenuItem *item) {
1129                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1130                         {
1131                             disp->oscdata->extraConfig.data[qq] = disp->storage->rand_pm1();
1132                         }
1133                     };
1134                     actionItem->setActions(action, nullptr);
1135                     contextMenu->addEntry(actionItem);
1136                 }
1137 
1138                 contextMenu->addSeparator();
1139 
1140                 {
1141                     auto actionItem = new CCommandMenuItem(
1142                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Absolute")));
1143                     auto action = [this](CCommandMenuItem *item) {
1144                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1145                         {
1146                             if (disp->oscdata->extraConfig.data[qq] < 0)
1147                             {
1148                                 disp->oscdata->extraConfig.data[qq] *= -1;
1149                             }
1150                         }
1151                     };
1152                     actionItem->setActions(action, nullptr);
1153                     contextMenu->addEntry(actionItem);
1154                 }
1155                 {
1156                     auto actionItem = new CCommandMenuItem(
1157                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Invert")));
1158                     auto action = [this](CCommandMenuItem *item) {
1159                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1160                         {
1161                             disp->oscdata->extraConfig.data[qq] =
1162                                 -disp->oscdata->extraConfig.data[qq];
1163                         }
1164                     };
1165                     actionItem->setActions(action, nullptr);
1166                     contextMenu->addEntry(actionItem);
1167                 }
1168                 {
1169                     auto actionItem = new CCommandMenuItem(
1170                         CCommandMenuItem::Desc(Surge::UI::toOSCaseForMenu("Reverse")));
1171                     auto action = [this](CCommandMenuItem *item) {
1172                         float pdata[AliasOscillator::n_additive_partials];
1173 
1174                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1175                         {
1176                             pdata[qq] = disp->oscdata->extraConfig.data[qq];
1177                         }
1178 
1179                         for (int qq = 0; qq < AliasOscillator::n_additive_partials; ++qq)
1180                         {
1181                             disp->oscdata->extraConfig.data[15 - qq] = pdata[qq];
1182                         }
1183                     };
1184                     actionItem->setActions(action, nullptr);
1185                     contextMenu->addEntry(actionItem);
1186                 }
1187 
1188                 disp->getFrame()->addView(contextMenu); // add to frame
1189                 contextMenu->setDirty();
1190                 contextMenu->popup();
1191 #if !TARGET_JUCE_UI
1192                 // wth is this?
1193                 contextMenu->onMouseDown(where, kLButton); // <-- modal menu loop is here
1194 #endif
1195                 // getFrame()->looseFocus(pContext);
1196 
1197                 disp->getFrame()->removeView(contextMenu, true); // remove from frame and forget
1198                 return kMouseDownEventHandledButDontNeedMovedOrUpEvents;
1199             }
1200 
1201             dragMode = EDIT;
1202             disp->startCursorHide();
1203             return onMouseMoved(where, buttons);
1204         }
1205 
1206         CMouseEventResult onMouseMoved(CPoint &where, const CButtonState &buttons) override
1207         {
1208             int ohs = hoveredSegment;
1209             int nhs = 0;
1210 
1211             for (auto &s : sliders)
1212             {
1213                 if (s.pointInside(where))
1214                 {
1215                     hoveredSegment = nhs;
1216                 }
1217                 nhs++;
1218             }
1219 
1220             if (hoveredSegment != ohs)
1221             {
1222                 disp->invalid();
1223             }
1224 
1225             if (dragMode == EDIT)
1226             {
1227                 int i = 0;
1228 
1229                 for (auto &s : sliders)
1230                 {
1231                     if ((where.x >= s.left) && (where.x <= s.right))
1232                     {
1233                         float f = (where.y - s.bottom) / (s.top - s.bottom);
1234                         f = (f - 0.5) * 2;
1235 
1236                         clamp1bp(f);
1237 
1238                         if ((buttons & kControl) || (buttons & kDoubleClick))
1239                         {
1240                             f = 0;
1241                         }
1242 
1243                         disp->oscdata->extraConfig.data[i] = f;
1244                         disp->invalid();
1245                         return kMouseEventHandled;
1246                     }
1247                     i++;
1248                 }
1249             }
1250 
1251             return kMouseEventHandled;
1252         }
1253 
1254         CMouseEventResult onMouseExited(CPoint &where, const CButtonState &buttons) override
1255         {
1256             return kMouseEventHandled;
1257         }
1258         CMouseEventResult onMouseEntered(CPoint &where, const CButtonState &buttons) override
1259         {
1260             return kMouseEventHandled;
1261         }
1262         CMouseEventResult onMouseUp(CPoint &where, const CButtonState &buttons) override
1263         {
1264             dragMode = NONE;
1265             disp->endCursorHide(where);
1266             return kMouseEventHandled;
1267         }
1268 
1269         bool onWheel(const VSTGUI::CPoint &where, const float &distance,
1270                      const VSTGUI::CButtonState &buttons) override
1271         {
1272             int idx = 0;
1273             for (auto &s : sliders)
1274             {
1275                 if (s.pointInside(where))
1276                 {
1277                     auto f = disp->oscdata->extraConfig.data[idx];
1278                     if (buttons & kShift)
1279                         f += 0.1 * distance / 3;
1280                     else
1281                         f += 0.1 * distance;
1282                     f = limit_range(f, -1.f, 1.f);
1283                     disp->oscdata->extraConfig.data[idx] = f;
1284                     disp->invalid();
1285                 }
1286                 idx++;
1287             }
1288             return true;
1289         }
1290 
1291         int hoveredSegment = -1;
1292         enum DragMode
1293         {
1294             NONE,
1295             EDIT
1296         } dragMode = NONE;
1297     };
1298 
1299     if (oscdata->type.val.i == ot_alias &&
1300         oscdata->p[AliasOscillator::ao_wave].val.i == AliasOscillator::aow_additive)
1301     {
1302         customEditor = std::make_shared<AliasAdditive>(this);
1303         customEditorActive = true;
1304         storage->getPatch()
1305             .dawExtraState.editor.oscExtraEditState[scene][osc_in_scene]
1306             .hasCustomEditor = true;
1307         return;
1308     }
1309 
1310     storage->getPatch()
1311         .dawExtraState.editor.oscExtraEditState[scene][osc_in_scene]
1312         .hasCustomEditor = false;
1313 }
1314 
closeCustomEditor()1315 void COscillatorDisplay::closeCustomEditor()
1316 {
1317     customEditorActive = false;
1318     customEditor = nullptr;
1319     storage->getPatch()
1320         .dawExtraState.editor.oscExtraEditState[scene][osc_in_scene]
1321         .hasCustomEditor = false;
1322 }
drawExtraEditButton(CDrawContext * dc,const std::string & label)1323 void COscillatorDisplay::drawExtraEditButton(CDrawContext *dc, const std::string &label)
1324 {
1325     auto size = getViewSize();
1326     CRect posn(size);
1327     // Position this button just below the bottom bounds line
1328     posn.bottom = getHeight() + posn.top - 1;
1329     posn.top = posn.bottom - wtbheight + 4;
1330     posn.right = posn.left + 45;
1331 
1332     customEditButtonRect = posn;
1333 
1334     if (editButtonHover)
1335     {
1336         dc->setFillColor(skin->getColor(Colors::Osc::Filename::BackgroundHover));
1337         dc->setFrameColor(skin->getColor(Colors::Osc::Filename::FrameHover));
1338         dc->setFontColor(skin->getColor(Colors::Osc::Filename::TextHover));
1339     }
1340     else
1341     {
1342         dc->setFillColor(skin->getColor(Colors::Osc::Filename::Background));
1343         dc->setFrameColor(skin->getColor(Colors::Osc::Filename::Frame));
1344         dc->setFontColor(skin->getColor(Colors::Osc::Filename::Text));
1345     }
1346 
1347     dc->drawRect(posn, kDrawFilledAndStroked);
1348     dc->setFont(Surge::GUI::getLatoAtSize(7, kBoldFace));
1349     dc->drawString(label.c_str(), posn, kCenterText, true);
1350 };
1351