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