diff --git a/Source/CrossPlatformUtils.h b/Source/CrossPlatformUtils.h index c0824a2..aaf56a5 100644 --- a/Source/CrossPlatformUtils.h +++ b/Source/CrossPlatformUtils.h @@ -4,7 +4,9 @@ #pragma once -void getSafeAreaInsets(void * component, float & top, float & bottom, float & left, float & right); +// notchPos is 0=none 1=top 2=bottom, 3=left, 4=right + +void getSafeAreaInsets(void * component, float & top, float & bottom, float & left, float & right, int & notchPos); #if JUCE_MAC diff --git a/Source/CrossPlatformUtilsIOS.mm b/Source/CrossPlatformUtilsIOS.mm new file mode 100644 index 0000000..e4b3367 --- /dev/null +++ b/Source/CrossPlatformUtilsIOS.mm @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception +// Copyright (C) 2020 Jesse Chappell + + + +#include "CrossPlatformUtils.h" + +//#include "JuceLibraryCode/AppConfig.h" + +#include + +#if JUCE_IOS + + + + +#import + +//#include "DebugLogC.h" + + + +//#include "../JuceLibraryCode/JuceHeader.h" + + +// notchPos is 0=none 1=top 2=bottom, 3=left, 4=right +void getSafeAreaInsets(void * component, float & top, float & bottom, float & left, float & right, int & notchPos) +{ + top = bottom = left = right = 0; + notchPos = 0; + + if ([(id)component isKindOfClass:[UIView class]]) { + UIView * view = (UIView *) component; + + if (@available(iOS 11, *)) { + UIEdgeInsets insets = view.safeAreaInsets; + top = insets.top; + bottom = insets.bottom; + left = insets.left; + right = insets.right; + + UIDeviceOrientation orient = [UIDevice currentDevice].orientation; + if ( orient == UIDeviceOrientationPortrait) { + notchPos = 1; + } else if (orient == UIDeviceOrientationPortraitUpsideDown) { + notchPos = 2; + } else if (orient == UIDeviceOrientationLandscapeLeft) { + notchPos = 3; + } else if (orient == UIDeviceOrientationLandscapeRight) { + notchPos = 4; + } + } + + //DebugLogC("Safe area insets of UIView: t: %g b: %g l: %g r:%g notch: %d", top, bottom, left, right, notchPos); + } + else { + top = bottom = left = right = 0; + //DebugLogC("NOT A UIVIEW"); + } +} + +#endif diff --git a/Source/CrossPlatformUtilsMac.mm b/Source/CrossPlatformUtilsMac.mm index 9423b71..e7af205 100644 --- a/Source/CrossPlatformUtilsMac.mm +++ b/Source/CrossPlatformUtilsMac.mm @@ -19,9 +19,10 @@ #import -void getSafeAreaInsets(void * component, float & top, float & bottom, float & left, float & right) +void getSafeAreaInsets(void * component, float & top, float & bottom, float & left, float & right, int & notchPos) { top = bottom = left = right = 0; + notchPos = 0; } diff --git a/Source/CustomLookAndFeel.cpp b/Source/CustomLookAndFeel.cpp new file mode 100644 index 0000000..4dc54ea --- /dev/null +++ b/Source/CustomLookAndFeel.cpp @@ -0,0 +1,1310 @@ +// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception +// Copyright (C) 2020 Jesse Chappell + + +#include "CustomLookAndFeel.h" + +//============================================================================== +CustomLookAndFeel::CustomLookAndFeel() +{ + // setColour (mainBackgroundColourId, Colour::greyLevel (0.8f)); + //DBG("Sonolook and feel"); + + setUsingNativeAlertWindows(true); + + fontScale = 1.0; + + + setColourScheme(getDarkColourScheme()); + + getCurrentColourScheme().setUIColour(ColourScheme::UIColour::windowBackground, Colour::fromFloatRGBA(0.0, 0.0, 0.0, 1.0)); + getCurrentColourScheme().setUIColour(ColourScheme::UIColour::widgetBackground, Colour::fromFloatRGBA(0.1, 0.1, 0.1, 1.0)); + getCurrentColourScheme().setUIColour(ColourScheme::UIColour::outline, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + + setColour (Label::textColourId, Colour (0xffcccccc)); + setColour (Label::textWhenEditingColourId, Colour (0xffe9e9e9)); + + setColour(ResizableWindow::backgroundColourId, Colour(0xff111111)); + + //setColour (TextButton::buttonColourId, Colour (0xff363636)); + setColour (TextButton::buttonColourId, Colour::fromFloatRGBA(0.15, 0.15, 0.15, 0.7)); // old one + //setColour (TextButton::buttonColourId, Colour::fromFloatRGBA(0.15, 0.15, 0.15, 0.0)); + //setColour (TextButton::buttonOnColourId, Colour (0xff3d70c8)); + setColour (TextButton::buttonOnColourId, Colour::fromFloatRGBA(0.5, 0.4, 0.6, 0.8)); + setColour (TextButton::textColourOnId, Colour (0xddcccccc)); + setColour (TextButton::textColourOffId, Colour (0xdde9e9e9)); + + setColour (ToggleButton::textColourId, Colour (0xddcccccc)); + + + setColour (ScrollBar::ColourIds::thumbColourId, Colour::fromFloatRGBA(0.4, 0.4, 0.4, 0.6)); + + //setColour (ComboBox::backgroundColourId, Colour (0xff161616)); + setColour (ComboBox::backgroundColourId, Colour::fromFloatRGBA(0.15, 0.15, 0.15, 0.7)); + setColour (ComboBox::textColourId, Colour (0xffe9e9e9)); + setColour (ComboBox::outlineColourId, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + + setColour (TextEditor::backgroundColourId, Colour (0xff050505)); + setColour (TextEditor::textColourId, Colour (0xffe9e9e9)); + setColour (TextEditor::highlightColourId, Colour (0xff5959f9)); + setColour (TextEditor::outlineColourId, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + setColour (TextEditor::focusedOutlineColourId, Colour::fromFloatRGBA(0.5, 0.5, 0.5, 0.7)); + + setColour (Slider::backgroundColourId, Colour::fromFloatRGBA(0.2, 0.2, 0.2, 1.0)); + setColour (Slider::rotarySliderOutlineColourId, Colour::fromFloatRGBA(0.2, 0.2, 0.2, 1.0)); + setColour (Slider::textBoxTextColourId, Colour(0xddcccccc)); + setColour (Slider::textBoxBackgroundColourId, Colour::fromFloatRGBA(0.05, 0.05, 0.05, 1.0)); + setColour (Slider::textBoxHighlightColourId, Colour (0xaa555555)); + setColour (Slider::textBoxOutlineColourId, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + + setColour (Slider::trackColourId, Colour::fromFloatRGBA(0.1, 0.4, 0.6, 0.8)); + setColour (Slider::thumbColourId, Colour::fromFloatRGBA(0.5, 0.4, 0.6, 0.9)); + //setColour (Slider::thumbColourId, Colour::fromFloatRGBA(0.2, 0.5, 0.7, 1.0)); + setColour (Slider::rotarySliderFillColourId, Colour::fromFloatRGBA(0.5, 0.4, 0.6, 0.9)); + + setColour (TabbedButtonBar::tabOutlineColourId, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + + + + setColour (ListBox::backgroundColourId, Colour::fromFloatRGBA(0.15, 0.15, 0.15, 0.7)); + setColour (ListBox::outlineColourId, Colour::fromFloatRGBA(0.3, 0.3, 0.3, 0.5)); + + setColour (BubbleComponent::backgroundColourId, Colour::fromFloatRGBA(0.25, 0.25, 0.25, 1.0)); + setColour (BubbleComponent::outlineColourId, Colour::fromFloatRGBA(0.4, 0.4, 0.4, 0.5)); + //setColour (TooltipWindow::textColourId, Colour(0xeecccccc)); + setColour (TooltipWindow::textColourId, Colour(0xee222222)); + setColour (TooltipWindow::backgroundColourId, Colour(0xeeffff99)); + + setColour (PopupMenu::backgroundColourId, Colour::fromFloatRGBA(0.2, 0.2, 0.2, 1.0)); + setColour (PopupMenu::highlightedBackgroundColourId, Colour::fromFloatRGBA(0.35, 0.35, 0.4, 1.0)); + + setColour (SidePanel::backgroundColour, Colour::fromFloatRGBA(0.17, 0.17, 0.17, 1.0)); + + + + //setColour (SonoDrawableButton::overOverlayColourId, Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.08)); + //setColour (SonoDrawableButton::downOverlayColourId, Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.3)); + + setColour (DrawableButton::textColourId, Colour (0xffb9b9b9)); + setColour (DrawableButton::textColourOnId, Colour (0xffe9e9e9)); + + //setColour (DrawableButton::backgroundColourId, Colour (0xffb9b9b9)); + setColour (DrawableButton::backgroundOnColourId, Colour::fromFloatRGBA(0.5, 0.4, 0.6, 0.8)); + + //setColour (ConfigurationRowView::backgroundColourId, Colour::fromFloatRGBA(0.05, 0.05, 0.05, 1.0)); + //setColour (ConfigurationRowView::selectedBackgroundColourId, Colour::fromFloatRGBA(0.15, 0.15, 0.15, 1.0)); + + setColour(ToggleButton::tickColourId, Colour::fromFloatRGBA(0.4, 0.8, 1.0, 1.0)); + + setColour (DirectoryContentsDisplayComponent::highlightColourId, Colour::fromFloatRGBA(0.1, 0.4, 0.6, 0.9)); + setColour (DirectoryContentsDisplayComponent::textColourId, Colour (0xffe9e9e9)); + // setColour (Label::textColourId, Colour (0xffe9e9e9)); + + //myFont = Typeface::createSystemTypefaceFor (BinaryData::DejaVuSans_ttf, BinaryData::DejaVuSans_ttfSize); + //setDefaultSansSerifTypefaceName("Gill Sans"); + //setDefaultSansSerifTypefaceName("Arial Unicode MS"); + //setDefaultSansSerifTypefaceName(myFont.getTypefaceName()); + + //myFont = Typeface::createSystemTypefaceFor (BinaryData::GillSans_ttc, BinaryData::GillSans_ttcSize); + myFont = Font(16 * fontScale); + + + if (auto * deflnf = dynamic_cast(&LookAndFeel::getDefaultLookAndFeel())) { + setLanguageCode(deflnf->languageCode); + } + + //DBG("Myfont name " << myFont.getTypefaceName()); +} + +void CustomLookAndFeel::setLanguageCode(const String & lang) +{ + languageCode = lang; + + if (lang.startsWith("zh")) { + fontScale = 1.0f; + } + else if (lang.startsWith("ko")) { + fontScale = 1.15f; + } +} + + + +Font CustomLookAndFeel::getMenuBarFont (MenuBarComponent& menuBar, int /*itemIndex*/, const String& /*itemText*/) +{ + return Font (menuBar.getHeight() * 0.7f); +} + +//============================================================================== +void CustomLookAndFeel::drawCallOutBoxBackground (CallOutBox& box, Graphics& g, + const Path& path, Image& cachedImage) +{ + if (cachedImage.isNull()) + { + cachedImage = Image (Image::ARGB, box.getWidth(), box.getHeight(), true); + Graphics g2 (cachedImage); + + DropShadow (Colours::black.withAlpha (0.7f), 8, Point (0, 2)).drawForPath (g2, path); + } + + g.setColour (Colours::black); + g.drawImageAt (cachedImage, 0, 0); + + //g.setColour (getCurrentColourScheme().getUIColour (ColourScheme::UIColour::widgetBackground).withAlpha (0.8f)); + g.setColour (getCurrentColourScheme().getUIColour (ColourScheme::UIColour::widgetBackground)); + g.fillPath (path); + + g.setColour (getCurrentColourScheme().getUIColour (ColourScheme::UIColour::outline).withAlpha (0.8f)); + g.strokePath (path, PathStrokeType (1.0f)); +} + + + +int CustomLookAndFeel::getTabButtonBestWidth (TabBarButton& button, int depth) +{ + return 250; // 120; +} + +int CustomLookAndFeel::getTabButtonSpaceAroundImage() { + return 0; +} + +static Colour getTabBackgroundColour (TabBarButton& button) +{ + + const Colour bkg (button.findColour (TabbedComponent::backgroundColourId).contrasting (0.15f)); + + if (button.isFrontTab()) + return bkg.overlaidWith (Colours::yellow.withAlpha (0.8f)); + + return bkg; +} + +Rectangle CustomLookAndFeel::getTabButtonExtraComponentBounds (const TabBarButton& button, Rectangle& textArea, Component& comp) +{ + Rectangle extraComp; + + auto orientation = button.getTabbedButtonBar().getOrientation(); + + if (button.getExtraComponentPlacement() == TabBarButton::beforeText) + { + switch (orientation) + { + case TabbedButtonBar::TabsAtBottom: + case TabbedButtonBar::TabsAtTop: extraComp = textArea.removeFromLeft (comp.getWidth()); break; + case TabbedButtonBar::TabsAtLeft: extraComp = textArea.removeFromBottom (comp.getHeight()); break; + case TabbedButtonBar::TabsAtRight: extraComp = textArea.removeFromTop (comp.getHeight()); break; + default: jassertfalse; break; + } + } + else if (button.getExtraComponentPlacement() == TabBarButton::afterText) + { + switch (orientation) + { + case TabbedButtonBar::TabsAtBottom: + case TabbedButtonBar::TabsAtTop: extraComp = textArea.removeFromRight (comp.getWidth()); break; + case TabbedButtonBar::TabsAtLeft: extraComp = textArea.removeFromTop (comp.getHeight()); break; + case TabbedButtonBar::TabsAtRight: extraComp = textArea.removeFromBottom (comp.getHeight()); break; + default: jassertfalse; break; + } + } + else if (button.getExtraComponentPlacement() == TabBarButton::aboveText) + { + switch (orientation) + { + case TabbedButtonBar::TabsAtBottom: + case TabbedButtonBar::TabsAtTop: extraComp = textArea.removeFromTop (comp.getHeight()); break; + case TabbedButtonBar::TabsAtLeft: extraComp = textArea.removeFromTop (comp.getHeight()); break; + case TabbedButtonBar::TabsAtRight: extraComp = textArea.removeFromTop (comp.getHeight()); break; + default: jassertfalse; break; + } + + // DBG("Extra comp bounds: " << extraComp.toString()) + extraComp.translate(0, 3); + //DBG("After Extra comp bounds: " << extraComp.toString()) + + } + else if (button.getExtraComponentPlacement() == TabBarButton::belowText) + { + switch (orientation) + { + case TabbedButtonBar::TabsAtBottom: + case TabbedButtonBar::TabsAtTop: extraComp = textArea.removeFromBottom (comp.getHeight()); break; + case TabbedButtonBar::TabsAtLeft: extraComp = textArea.removeFromBottom (comp.getHeight()); break; + case TabbedButtonBar::TabsAtRight: extraComp = textArea.removeFromBottom (comp.getHeight()); break; + default: jassertfalse; break; + } + } + + return extraComp; +} + +void CustomLookAndFeel::createTabTextLayout (const TabBarButton& button, float length, float depth, + Colour colour, TextLayout& textLayout) +{ + float fontsize = button.getExtraComponent() != nullptr ? jmin(depth, 32.0f) * 0.85f : jmin(depth, 32.0f) * 0.5f; + Font font = myFont.withHeight(fontsize * fontScale); + font.setUnderline (button.hasKeyboardFocus (false)); + + AttributedString s; + s.setWordWrap(AttributedString::byWord); + s.setJustification (Justification::centred); + s.append (button.getButtonText().trim(), font, colour); + + textLayout.createLayout (s, length); +} + +void CustomLookAndFeel::drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown) +{ + const Rectangle activeArea (button.getActiveArea()); + + const TabbedButtonBar::Orientation o = button.getTabbedButtonBar().getOrientation(); + + const Colour bkg (button.getTabBackgroundColour()); + const Colour selcol = Colour::fromFloatRGBA(0.0f, 0.2f, 0.4f, 1.0f); + + // DBG("Sono draw tab button"); + + + if (button.getToggleState() && bkg != Colours::black) + { + //g.setColour (bkg); + g.setColour (selcol); + } + else + { + + Point p1, p2; + + switch (o) + { + case TabbedButtonBar::TabsAtBottom: p1 = activeArea.getBottomLeft(); p2 = activeArea.getTopLeft(); break; + case TabbedButtonBar::TabsAtTop: p1 = activeArea.getTopLeft(); p2 = activeArea.getBottomLeft(); break; + case TabbedButtonBar::TabsAtRight: p1 = activeArea.getTopRight(); p2 = activeArea.getTopLeft(); break; + case TabbedButtonBar::TabsAtLeft: p1 = activeArea.getTopLeft(); p2 = activeArea.getTopRight(); break; + default: jassertfalse; break; + } + + g.setColour(isMouseDown ? bkg.brighter(0.1) : bkg); + + //g.setGradientFill (ColourGradient (bkg.darker (0.1f), (float) p1.x, (float) p1.y, + // bkg.darker (0.5f), (float) p2.x, (float) p2.y, false)); + } + + Rectangle p (activeArea.reduced(1)); + + g.fillRect (p); + + //g.fillRect (activeArea); + +#if 0 + g.setColour (button.findColour (TabbedButtonBar::tabOutlineColourId)); + + Rectangle r (activeArea); + + if (o != TabbedButtonBar::TabsAtBottom) g.fillRect (r.removeFromTop (1)); + if (o != TabbedButtonBar::TabsAtTop) g.fillRect (r.removeFromBottom (1)); + if (o != TabbedButtonBar::TabsAtRight) g.fillRect (r.removeFromLeft (1)); + if (o != TabbedButtonBar::TabsAtLeft) g.fillRect (r.removeFromRight (1)); +#endif + + + const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f; + + Colour col (bkg.contrasting().withMultipliedAlpha (alpha)); + + if (TabbedButtonBar* bar = button.findParentComponentOfClass()) + { + TabbedButtonBar::ColourIds colID = button.isFrontTab() ? TabbedButtonBar::frontTextColourId + : TabbedButtonBar::tabTextColourId; + + if (bar->isColourSpecified (colID)) + col = bar->findColour (colID); + else if (isColourSpecified (colID)) + col = findColour (colID); + } + + const Rectangle area (button.getTextArea().toFloat()); + + float length = area.getWidth(); + float depth = area.getHeight(); + + if (button.getTabbedButtonBar().isVertical()) + std::swap (length, depth); + + TextLayout textLayout; + createTabTextLayout (button, length, depth, col, textLayout); + + AffineTransform t; + + switch (o) + { + case TabbedButtonBar::TabsAtLeft: t = t.rotated (float_Pi * -0.5f).translated (area.getX(), area.getBottom()); break; + case TabbedButtonBar::TabsAtRight: t = t.rotated (float_Pi * 0.5f).translated (area.getRight(), area.getY()); break; + case TabbedButtonBar::TabsAtTop: + case TabbedButtonBar::TabsAtBottom: t = t.translated (area.getX(), area.getY()); break; + default: jassertfalse; break; + } + + g.addTransform (t); + textLayout.draw (g, Rectangle (length, depth)); +} + +/* +void CustomLookAndFeel::drawTabButton (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown) +{ + const Rectangle activeArea (button.getActiveArea()); + + const Colour bkg (getTabBackgroundColour (button)); + + g.setGradientFill (ColourGradient (bkg.brighter (0.1f), 0, (float) activeArea.getY(), + bkg.darker (0.1f), 0, (float) activeArea.getBottom(), false)); + g.fillRect (activeArea); + + g.setColour (button.findColour (TabbedComponent::backgroundColourId).darker (0.3f)); + g.drawRect (activeArea); + + const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f; + const Colour col (bkg.contrasting().withMultipliedAlpha (alpha)); + + TextLayout textLayout; + LookAndFeel_V3::createTabTextLayout (button, (float) activeArea.getWidth(), (float) activeArea.getHeight(), col, textLayout); + + textLayout.draw (g, button.getTextArea().toFloat()); +} +*/ + +void CustomLookAndFeel::drawTabbedButtonBarBackground (TabbedButtonBar&, Graphics&) {} + +void CustomLookAndFeel::drawTabAreaBehindFrontButton (TabbedButtonBar& bar, Graphics& g, const int w, const int h) +{ + const float shadowSize = 0.15f; + + Rectangle shadowRect, line; + ColourGradient gradient (Colours::black.withAlpha (bar.isEnabled() ? 0.08f : 0.04f), 0, 0, + Colours::transparentBlack, 0, 0, false); + + switch (bar.getOrientation()) + { + case TabbedButtonBar::TabsAtLeft: + gradient.point1.x = (float) w; + gradient.point2.x = w * (1.0f - shadowSize); + shadowRect.setBounds ((int) gradient.point2.x, 0, w - (int) gradient.point2.x, h); + line.setBounds (w - 1, 0, 1, h); + break; + + case TabbedButtonBar::TabsAtRight: + gradient.point2.x = w * shadowSize; + shadowRect.setBounds (0, 0, (int) gradient.point2.x, h); + line.setBounds (0, 0, 1, h); + break; + + case TabbedButtonBar::TabsAtTop: + gradient.point1.y = (float) h; + gradient.point2.y = h * (1.0f - shadowSize); + shadowRect.setBounds (0, (int) gradient.point2.y, w, h - (int) gradient.point2.y); + line.setBounds (0, h - 1, w, 1); + break; + + case TabbedButtonBar::TabsAtBottom: + gradient.point2.y = h * shadowSize; + shadowRect.setBounds (0, 0, w, (int) gradient.point2.y); + line.setBounds (0, 0, w, 1); + break; + + default: break; + } + + g.setGradientFill (gradient); + g.fillRect (shadowRect.expanded (2, 2)); + + g.setColour (bar.findColour (TabbedButtonBar::tabOutlineColourId)); + g.fillRect (line); +} + +void CustomLookAndFeel::drawTabButtonText (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown) +{ + const Rectangle area (button.getTextArea().toFloat()); + + //DBG("Sono look and feel drawtabbutton text: " << button.getButtonText()); + + float length = area.getWidth(); + float depth = area.getHeight(); + + if (button.getTabbedButtonBar().isVertical()) + std::swap (length, depth); + + Font font = myFont.withHeight(jmin(depth,30.0f) * 0.6f * fontScale); + font.setUnderline (button.hasKeyboardFocus (false)); + + AffineTransform t; + + switch (button.getTabbedButtonBar().getOrientation()) + { + case TabbedButtonBar::TabsAtLeft: t = t.rotated (float_Pi * -0.5f).translated (area.getX(), area.getBottom()); break; + case TabbedButtonBar::TabsAtRight: t = t.rotated (float_Pi * 0.5f).translated (area.getRight(), area.getY()); break; + case TabbedButtonBar::TabsAtTop: + case TabbedButtonBar::TabsAtBottom: t = t.translated (area.getX(), area.getY()); break; + default: jassertfalse; break; + } + + Colour col; + + if (button.isFrontTab() && (button.isColourSpecified (TabbedButtonBar::frontTextColourId) + || isColourSpecified (TabbedButtonBar::frontTextColourId))) + col = findColour (TabbedButtonBar::frontTextColourId); + else if (button.isColourSpecified (TabbedButtonBar::tabTextColourId) + || isColourSpecified (TabbedButtonBar::tabTextColourId)) + col = findColour (TabbedButtonBar::tabTextColourId); + else + col = button.getTabBackgroundColour().contrasting(); + + const float alpha = button.isEnabled() ? ((isMouseOver || isMouseDown) ? 1.0f : 0.8f) : 0.3f; + + g.setColour (col.withMultipliedAlpha (alpha)); + g.setFont (font); + g.addTransform (t); + + g.drawFittedText (button.getButtonText().trim(), + 0, 0, (int) length, (int) depth, + Justification::centred, + //jmax (1, ((int) depth) / 12), 0.5f); + 1, 0.5f); +} + +static Range getBrightnessRange (const Image& im) +{ + float minB = 1.0f, maxB = 0; + const int w = im.getWidth(); + const int h = im.getHeight(); + + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + const float b = im.getPixelAt (x, y).getBrightness(); + minB = jmin (minB, b); + maxB = jmax (maxB, b); + } + } + + return Range (minB, maxB); +} + +Font CustomLookAndFeel::getLabelFont (Label& label) +{ + if (fontScale == 1.0f) { + return label.getFont(); + } + else { + return label.getFont().withHeight(label.getFont().getHeight() * fontScale); + } +} + +void CustomLookAndFeel::drawLabel (Graphics& g, Label& label) +{ + Colour olcolor = label.findColour (Label::backgroundColourId); + + g.setColour(olcolor); + if (!olcolor.isTransparent()) { + if (labelCornerRadius > 0.0f) { + g.fillRoundedRectangle(label.getLocalBounds().reduced(1).toFloat(), labelCornerRadius); + } else { + g.fillAll (olcolor); + } + } + + + if (! label.isBeingEdited()) + { + auto alpha = label.isEnabled() ? 1.0f : 0.5f; + const Font font (getLabelFont (label)); + + g.setColour (label.findColour (Label::textColourId).withMultipliedAlpha (alpha)); + g.setFont (font); + + auto textArea = getLabelBorderSize (label).subtractedFrom (label.getLocalBounds()); + + g.drawFittedText (label.getText(), textArea, label.getJustificationType(), + jmax (1, (int) (textArea.getHeight() / font.getHeight())), + label.getMinimumHorizontalScale()); + + olcolor = label.findColour (Label::outlineColourId).withMultipliedAlpha (alpha); + } + else if (label.isEnabled()) + { + olcolor = label.findColour (Label::outlineColourId); + } + + if (!olcolor.isTransparent()) { + g.setColour (olcolor); + if (labelCornerRadius > 0.0f) { + g.drawRoundedRectangle(label.getLocalBounds().reduced(1).toFloat(), labelCornerRadius, 1.0f); + } else { + g.drawRect (label.getLocalBounds()); + } + } +} + +Font CustomLookAndFeel::getTextButtonFont (TextButton& button, int buttonHeight) +{ + // DBG("GetTextButton font with height: " << buttonHeight); + float textRatio = 0.5f; + //if (SonoTextButton* const textbutt = dynamic_cast (&button)) { + // textRatio = textbutt->getTextHeightRatio(); + //} + + return myFont.withHeight(jmin (16.0f, buttonHeight * textRatio) * fontScale); +} + +Button* CustomLookAndFeel::createSliderButton (Slider&, const bool isIncrement) +{ + TextButton * butt = new TextButton (isIncrement ? "+" : "-", {}); + return butt; +} + +Label* CustomLookAndFeel::createSliderTextBox (Slider& slider) +{ + Label * lab = LookAndFeel_V4::createSliderTextBox(slider); + lab->setKeyboardType(TextInputTarget::decimalKeyboard); + lab->setFont(myFont.withHeight(16.0* fontScale)); + lab->setMinimumHorizontalScale(0.5); + lab->setJustificationType(Justification::centredRight); + return lab; +} + +Font CustomLookAndFeel::getSliderPopupFont (Slider&) +{ + return Font (18.0f, Font::bold); +} + +int CustomLookAndFeel::getSliderPopupPlacement (Slider&) +{ + return BubbleComponent::above + //| BubbleComponent::below + | BubbleComponent::left + | BubbleComponent::right + ; +} + +void CustomLookAndFeel::drawButtonTextWithAlignment (Graphics& g, TextButton& button, bool /*isMouseOverButton*/, bool /*isButtonDown*/, Justification textjust) +{ + Font font (getTextButtonFont (button, button.getHeight())); + g.setFont (font); + g.setColour (button.findColour (button.getToggleState() ? TextButton::textColourOnId + : TextButton::textColourOffId) + .withMultipliedAlpha (button.isEnabled() ? 1.0f : 0.5f)); + + float textRatio = 0.7f; + //if (SonoTextButton* const textbutt = dynamic_cast (&button)) { + // textRatio = textbutt->getTextHeightRatio(); + //} + + const int yIndent = jmin (2, button.proportionOfHeight ((1.0 - textRatio) * 0.5)); + const int cornerSize = jmin (button.getHeight(), button.getWidth()) / 2; + + const int fontHeight = roundToInt (font.getHeight() * 0.3); + const int leftIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnLeft() ? 4 : 2)); + const int rightIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnRight() ? 4 : 2)); + + g.drawFittedText (button.getButtonText(), + leftIndent, + yIndent, + button.getWidth() - leftIndent - rightIndent, + button.getHeight() - yIndent * 2, + textjust, 2, 0.7f); +} + + +void CustomLookAndFeel::drawButtonText (Graphics& g, TextButton& button, bool isMouseOverButton, bool isButtonDown) +{ + drawButtonTextWithAlignment(g, button, isMouseOverButton, isButtonDown); +} + + +void CustomLookAndFeel::drawFileBrowserRow (Graphics& g, int width, int height, + const File& file, const String& filename, Image* icon, + const String& fileSizeDescription, + const String& fileTimeDescription, + bool isDirectory, bool isItemSelected, + int itemIndex, DirectoryContentsDisplayComponent& dcc) +{ + Component* const fileListComp = dynamic_cast (&dcc); + + if (isItemSelected) + g.fillAll (fileListComp != nullptr ? fileListComp->findColour (DirectoryContentsDisplayComponent::highlightColourId) + : findColour (DirectoryContentsDisplayComponent::highlightColourId)); + + int x = 32; + g.setColour (Colours::black); + + if (isDirectory) { + if (icon != nullptr && icon->isValid()) + { + g.drawImageWithin (*icon, 2, 2, x - 4, height - 4, + RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, + false); + } + else + { + if (const Drawable* d = isDirectory ? getDefaultFolderImage() + : getDefaultDocumentFileImage()) + d->drawWithin (g, Rectangle (2.0f, 2.0f, x - 4.0f, height - 4.0f), + RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, 1.0f); + } + } + else { + x = 4; + } + + g.setColour (fileListComp != nullptr ? fileListComp->findColour (DirectoryContentsDisplayComponent::textColourId) + : findColour (DirectoryContentsDisplayComponent::textColourId)); + g.setFont (myFont.withHeight(height * 0.5f)); + + if (width > 450 && ! isDirectory) + { + const int sizeX = roundToInt (width * 0.7f); + const int dateX = roundToInt (width * 0.8f); + + g.drawFittedText (filename, + x, 0, sizeX - x, height, + Justification::centredLeft, 1); + + g.setFont (myFont.withHeight(height * 0.5f)); + g.setColour (Colours::darkgrey); + + if (! isDirectory) + { + g.drawFittedText (fileSizeDescription, + sizeX, 0, dateX - sizeX - 8, height, + Justification::centredRight, 1); + + g.drawFittedText (fileTimeDescription, + dateX, 0, width - 8 - dateX, height, + Justification::centredRight, 1); + } + } + else + { + g.drawFittedText (filename, + x, 0, width - x, height, + Justification::centredLeft, 1); + + } +} + +Button* CustomLookAndFeel::createFileBrowserGoUpButton() +{ + DrawableButton* goUpButton = new DrawableButton ("up", DrawableButton::ImageOnButtonBackground); + + Path arrowPath; + arrowPath.addArrow (Line (50.0f, 100.0f, 50.0f, 0.0f), 40.0f, 100.0f, 50.0f); + + DrawablePath arrowImage; + arrowImage.setFill (Colours::white.withAlpha (0.4f)); + arrowImage.setPath (arrowPath); + + goUpButton->setImages (&arrowImage); + + return goUpButton; +} + +void CustomLookAndFeel::layoutFileBrowserComponent (FileBrowserComponent& browserComp, + DirectoryContentsDisplayComponent* fileListComponent, + FilePreviewComponent* previewComp, + ComboBox* currentPathBox, + TextEditor* filenameBox, + Button* goUpButton) +{ + const int x = 8; + int w = browserComp.getWidth() - x - x; + + if (previewComp != nullptr) + { + const int previewWidth = w / 3; + previewComp->setBounds (x + w - previewWidth, 0, previewWidth, browserComp.getHeight()); + + w -= previewWidth + 4; + } + + int y = 4; + + const int controlsHeight = 22; + const int bottomSectionHeight = controlsHeight + 8; + const int upButtonWidth = 50; + + currentPathBox->setBounds (x, y, w - upButtonWidth - 6, controlsHeight); + goUpButton->setBounds (x + w - upButtonWidth, y, upButtonWidth, controlsHeight); + + y += controlsHeight + 4; + + if (Component* const listAsComp = dynamic_cast (fileListComponent)) + { + listAsComp->setBounds (x, y, w, browserComp.getHeight() - y - bottomSectionHeight); + y = listAsComp->getBottom() + 4; + } + + filenameBox->setBounds (x + 50, y, w - 50, controlsHeight); +} + + +PopupMenu::Options CustomLookAndFeel::getOptionsForComboBoxPopupMenu (ComboBox& box, Label& label) +{ + auto options = PopupMenu::Options().withTargetComponent (&box) + .withItemThatMustBeVisible (box.getSelectedId()) + .withMinimumWidth (box.getWidth()) + .withMaximumNumColumns (1) + .withStandardItemHeight (label.getHeight()); + +#if JUCE_IOS || JUCE_ANDROID + auto * dw = box.findParentComponentOfClass(); + if (dw) { + options = options.withParentComponent(dw); + } +#endif + + return options; +} + +void CustomLookAndFeel::drawTreeviewPlusMinusBox (Graphics& g, const Rectangle& area, + Colour backgroundColour, bool isOpen, bool isMouseOver) +{ + Path p; + p.addTriangle (0.0f, 0.0f, 1.0f, isOpen ? 0.0f : 0.5f, isOpen ? 0.5f : 0.0f, 1.0f); + + DBG("draw plus minus ours"); + + //g.setColour (backgroundColour.contrasting().withAlpha (isMouseOver ? 0.5f : 0.3f)); + g.setColour (Colours::white.withAlpha (isMouseOver ? 0.5f : 0.3f)); + g.fillPath (p, p.getTransformToScaleToFit (area.reduced (2, area.getHeight() / 4), true)); +} + + +void CustomLookAndFeel::drawToggleButton (Graphics& g, ToggleButton& button, + bool isMouseOverButton, bool isButtonDown) +{ + /* + if (button.hasKeyboardFocus (true)) + { + g.setColour (button.findColour (TextEditor::focusedOutlineColourId)); + g.drawRect (0, 0, button.getWidth(), button.getHeight()); + } + */ + + float fontSize = jmin (15.0f, button.getHeight() * 0.75f) * fontScale; + const float tickWidth = fontSize * 1.1f; + + drawTickBox (g, button, 4.0f, (button.getHeight() - tickWidth) * 0.5f, + tickWidth, tickWidth, + button.getToggleState(), + button.isEnabled(), + isMouseOverButton, + isButtonDown); + + g.setColour (button.findColour (ToggleButton::textColourId)); + g.setFont (myFont.withHeight(fontSize)); + + if (! button.isEnabled()) + g.setOpacity (0.5f); + + const int textX = (int) tickWidth + 10; + + g.drawFittedText (button.getButtonText(), + textX, 0, + button.getWidth() - textX - 2, button.getHeight(), + Justification::centredLeft, 10); +} + +void CustomLookAndFeel::drawTickBox (Graphics& g, Component& component, + float x, float y, float w, float h, + const bool ticked, + const bool isEnabled, + const bool isMouseOverButton, + const bool isButtonDown) +{ + const float boxSize = w * 1.0f; + + + g.setColour (component.findColour (TextEditor::focusedOutlineColourId)); + g.drawRect (x, y + (h - boxSize) * 0.5f, boxSize, boxSize); + + if (ticked) + { + Path tick; + tick.startNewSubPath (1.5f, 3.0f); + tick.lineTo (3.0f, 6.0f); + tick.lineTo (6.0f, 0.0f); + + + g.setColour (isEnabled ? component.findColour(ToggleButton::tickColourId) : Colours::grey); + + const AffineTransform trans (AffineTransform::scale (w / 9.0f, h / 9.0f) + .translated (x+2, y+1)); + + g.strokePath (tick, PathStrokeType (2.5f), trans); + } +} + +void CustomLookAndFeel::drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos, + const float rotaryStartAngle, const float rotaryEndAngle, Slider& slider) +{ + const auto outline = findColour (Slider::rotarySliderOutlineColourId); + const auto fill = findColour (Slider::rotarySliderFillColourId); + + const auto bounds = Rectangle (x, y, width, height).toFloat().reduced (3); + + + auto radius = jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f; + const auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + auto lineW = jmin (8.0f, radius * 0.3f); + auto arcRadius = radius - lineW * 0.5f; + + Path backgroundArc; + backgroundArc.addCentredArc (bounds.getCentreX(), + bounds.getCentreY(), + arcRadius, + arcRadius, + 0.0f, + rotaryStartAngle, + rotaryEndAngle, + true); + + g.setColour (outline); + g.strokePath (backgroundArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded)); + + auto rotStartAngle = rotaryStartAngle; + + if (slider.getProperties().contains ("fromCentre")) + { + rotStartAngle = (rotStartAngle + rotaryEndAngle) / 2; + } + + + if (slider.isEnabled()) + { + Path valueArc; + valueArc.addCentredArc (bounds.getCentreX(), + bounds.getCentreY(), + arcRadius, + arcRadius, + 0.0f, + rotStartAngle, + toAngle, + true); + + g.setColour (fill); + g.strokePath (valueArc, PathStrokeType (lineW, PathStrokeType::curved, PathStrokeType::rounded)); + } + + const auto thumbWidth = lineW ; // * 1.5f; + const Point thumbPoint (bounds.getCentreX() + arcRadius * std::cos (toAngle - float_Pi * 0.5f), + bounds.getCentreY() + arcRadius * std::sin (toAngle - float_Pi * 0.5f)); + + g.setColour (findColour (Slider::thumbColourId)); + g.fillEllipse (Rectangle (thumbWidth, thumbWidth).withCentre (thumbPoint)); +} + +//============================================================================== +int CustomLookAndFeel::getSliderThumbRadius (Slider& slider) +{ + if (slider.isTwoValue() || slider.isThreeValue()) { + return jmin (14, slider.isHorizontal() ? static_cast (slider.getHeight() * 0.25f) + : static_cast (slider.getWidth() * 0.5f)); + } + return jmin (16, slider.isHorizontal() ? static_cast (slider.getHeight() * 0.5f) + : static_cast (slider.getWidth() * 0.5f)); +} + + +Slider::SliderLayout CustomLookAndFeel::getSliderLayout (Slider& slider) +{ + // 1. compute the actually visible textBox size from the slider textBox size and some additional constraints + + int minXSpace = 0; + int minYSpace = 0; + + auto textBoxPos = slider.getTextBoxPosition(); + + if (textBoxPos == Slider::TextBoxLeft || textBoxPos == Slider::TextBoxRight) + minXSpace = 30; + else + minYSpace = 15; + + auto localBounds = slider.getLocalBounds(); + + auto textBoxWidth = jmax (0, jmin (slider.getTextBoxWidth(), localBounds.getWidth() - minXSpace)); + auto textBoxHeight = jmax (0, jmin (slider.getTextBoxHeight(), localBounds.getHeight() - minYSpace)); + + Slider::SliderLayout layout; + + // 2. set the textBox bounds + + if (textBoxPos != Slider::NoTextBox) + { + if (slider.isBar()) + { + layout.textBoxBounds = localBounds; + } + else + { + layout.textBoxBounds.setWidth (textBoxWidth); + layout.textBoxBounds.setHeight (textBoxHeight); + + const int thumbIndent = getSliderThumbRadius (slider); + + if (textBoxPos == Slider::TextBoxLeft) layout.textBoxBounds.setX (0); + else if (textBoxPos == Slider::TextBoxRight) layout.textBoxBounds.setX (localBounds.getWidth() - textBoxWidth); + else if (sliderTextJustification.testFlags(Justification::right))/* above or below -> right */ layout.textBoxBounds.setX ((localBounds.getWidth() - textBoxWidth - 1)); + else if (sliderTextJustification.testFlags(Justification::left))/* above or below -> left */ layout.textBoxBounds.setX (1); + else /* above or below -> centre horizontally */ layout.textBoxBounds.setX ((localBounds.getWidth() - textBoxWidth) / 2); + + if (textBoxPos == Slider::TextBoxAbove) layout.textBoxBounds.setY (0); + else if (textBoxPos == Slider::TextBoxBelow) layout.textBoxBounds.setY (localBounds.getHeight() - textBoxHeight); + else if (sliderTextJustification.testFlags(Justification::top))/* left or right -> top */ layout.textBoxBounds.setY (0); + else if (sliderTextJustification.testFlags(Justification::bottom))/* left or right -> bottom */ layout.textBoxBounds.setY (localBounds.getHeight() - textBoxHeight); + else /* left or right -> centre vertically */ layout.textBoxBounds.setY ((localBounds.getHeight() - textBoxHeight) / 2); + } + } + + // 3. set the slider bounds + + layout.sliderBounds = localBounds; + + if (slider.isBar()) + { + layout.sliderBounds.reduce (1, 1); // bar border + } + else + { + if (textBoxPos == Slider::TextBoxLeft) layout.sliderBounds.removeFromLeft (textBoxWidth); + else if (textBoxPos == Slider::TextBoxRight) layout.sliderBounds.removeFromRight (textBoxWidth); + else if (textBoxPos == Slider::TextBoxAbove) layout.sliderBounds.removeFromTop (textBoxHeight); + else if (textBoxPos == Slider::TextBoxBelow) layout.sliderBounds.removeFromBottom (textBoxHeight); + + const int thumbIndent = getSliderThumbRadius (slider); + + if (slider.isHorizontal()) layout.sliderBounds.reduce (thumbIndent, 0); + else if (slider.isVertical()) layout.sliderBounds.reduce (0, thumbIndent); + } + + return layout; +} + + +void CustomLookAndFeel::drawLinearSlider (Graphics& g, int x, int y, int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const Slider::SliderStyle style, Slider& slider) +{ + if (slider.isBar()) + { + if (slider.getProperties().contains ("fromCentre")) { + auto centrex = x + width*0.5f; + auto centrey = y + height*0.5f; + + if (!slider.getProperties().contains ("noFill")) { + g.setColour (slider.findColour (Slider::trackColourId)); + + g.fillRect (slider.isHorizontal() ? Rectangle (sliderPos > centrex ? centrex : sliderPos, y + 0.5f, sliderPos > centrex ? sliderPos - centrex : centrex - sliderPos, height - 1.0f) + : Rectangle (x + 0.5f, sliderPos < centrey ? sliderPos : centrey, width - 1.0f, sliderPos < centrey ? centrey - sliderPos : sliderPos - centrey)); + } + + // draw line + g.setColour (slider.findColour (Slider::thumbColourId)); + + g.fillRect (slider.isHorizontal() ? Rectangle (sliderPos - 1, y + 0.5f, 2, height - 1.0f) + : Rectangle (x + 0.5f, sliderPos - 1, width - 1.0f, 2)); + } + else { + + if (!slider.getProperties().contains ("noFill")) { + + g.setColour (slider.findColour (Slider::trackColourId)); + + g.fillRect (slider.isHorizontal() ? Rectangle (static_cast (x), y + 0.5f, sliderPos - x, height - 1.0f) + : Rectangle (x + 0.5f, sliderPos, width - 1.0f, y + (height - sliderPos))); + } + //else + + g.setColour (slider.findColour (Slider::thumbColourId)); + + { + // draw line + g.fillRect (slider.isHorizontal() ? Rectangle (sliderPos - 1, y + 0.5f, 3, height - 1.0f) + : Rectangle (x + 0.5f, sliderPos - 1, width - 1.0f, 3)); + } + } + } + else + { + auto isTwoVal = (style == Slider::SliderStyle::TwoValueVertical || style == Slider::SliderStyle::TwoValueHorizontal); + auto isThreeVal = (style == Slider::SliderStyle::ThreeValueVertical || style == Slider::SliderStyle::ThreeValueHorizontal); + + auto trackWidth = jmin (10.0f, slider.isHorizontal() ? height * 0.25f : width * 0.25f); + + Point startPoint (slider.isHorizontal() ? x : x + width * 0.5f, + slider.isHorizontal() ? y + height * 0.5f : height + y); + + Point endPoint (slider.isHorizontal() ? width + x : startPoint.x, + slider.isHorizontal() ? startPoint.y : y); + + Path backgroundTrack; + backgroundTrack.startNewSubPath (startPoint); + backgroundTrack.lineTo (endPoint); + g.setColour (slider.findColour (Slider::backgroundColourId)); + g.strokePath (backgroundTrack, { trackWidth, PathStrokeType::curved, PathStrokeType::rounded }); + + Path valueTrack; + Point minPoint, maxPoint, thumbPoint; + + if (isTwoVal || isThreeVal) + { + minPoint = { slider.isHorizontal() ? minSliderPos : width * 0.5f, + slider.isHorizontal() ? height * 0.5f : minSliderPos }; + + if (isThreeVal) + thumbPoint = { slider.isHorizontal() ? sliderPos : width * 0.5f, + slider.isHorizontal() ? height * 0.5f : sliderPos }; + + maxPoint = { slider.isHorizontal() ? maxSliderPos : width * 0.5f, + slider.isHorizontal() ? height * 0.5f : maxSliderPos }; + } + else + { + auto kx = slider.isHorizontal() ? sliderPos : (x + width * 0.5f); + auto ky = slider.isHorizontal() ? (y + height * 0.5f) : sliderPos; + + minPoint = startPoint; + maxPoint = { kx, ky }; + } + + auto thumbWidth = getSliderThumbRadius (slider); + + valueTrack.startNewSubPath (minPoint); + valueTrack.lineTo (isThreeVal ? thumbPoint : maxPoint); + g.setColour (slider.findColour (Slider::trackColourId)); + g.strokePath (valueTrack, { trackWidth, PathStrokeType::curved, PathStrokeType::rounded }); + + if (! isTwoVal) + { + g.setColour (slider.findColour (Slider::thumbColourId)); + g.fillEllipse (Rectangle (static_cast (thumbWidth), static_cast (thumbWidth)).withCentre (isThreeVal ? thumbPoint : maxPoint)); + } + + if (isTwoVal || isThreeVal) + { + auto sr = jmin (trackWidth, (slider.isHorizontal() ? height : width) * 0.4f); + auto pointerColour = slider.findColour (Slider::thumbColourId); + + auto wscale = 1.5f; + + if (slider.isHorizontal()) + { + /* + drawPointer (g, minSliderPos - sr, + height * 0.5f - trackWidth*wscale*0.5, //jmax (0.0f, y + height * 0.5f - trackWidth * 0.5f), + trackWidth * wscale, pointerColour, 1); // 2 + + drawPointer (g, maxSliderPos - trackWidth, + height * 0.5f - trackWidth*wscale*0.5, //jmin (y + height - trackWidth * 2.0f, (float)y + height * 0.5f), + trackWidth * wscale, pointerColour, 3); // 4 + */ + + drawPointer (g, minSliderPos - sr, + jmax (0.0f, y + height * 0.5f - trackWidth * wscale), + trackWidth * wscale, pointerColour, 2); // 2 + + drawPointer (g, maxSliderPos - trackWidth*0.5*wscale, + jmin (y + height - trackWidth * wscale, (float)y + height * 0.5f), + trackWidth * wscale, pointerColour, 4); // 4 + } + else + { + drawPointer (g, jmax (0.0f, x + width * 0.5f - trackWidth * 2.0f), + minSliderPos - trackWidth, + trackWidth * wscale, pointerColour, 1); + + drawPointer (g, jmin (x + width - trackWidth * 2.0f, x + width * 0.5f), maxSliderPos - sr, + trackWidth * wscale, pointerColour, 3); + } + } + } +} + + +void CustomLookAndFeel::drawDrawableButton (Graphics& g, DrawableButton& button, + bool isMouseOverButton, bool isButtonDown) +{ + const auto cornerSize = 6.0f; + bool toggleState = button.getToggleState(); + + ; + + //Rectangle bounds = g.getClipBounds().toFloat(); + Rectangle bounds = button.getLocalBounds().toFloat(); + + g.setColour(button.findColour (toggleState ? DrawableButton::backgroundOnColourId + : DrawableButton::backgroundColourId)); + //g.fillAll(); + g.fillRoundedRectangle(bounds, cornerSize); + + if (isButtonDown) { + //setColour (SonoDrawableButton::overOverlayColourId, Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.08)); + //setColour (SonoDrawableButton::downOverlayColourId, Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.3)); + g.setColour(Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.3)); + + //g.setColour(findColour(SonoDrawableButton::downOverlayColourId)); + //g.fillAll(); + g.fillRoundedRectangle(bounds, cornerSize); + } + else if (isMouseOverButton) { + //g.setColour(findColour(SonoDrawableButton::overOverlayColourId)); + g.setColour(Colour::fromFloatRGBA(0.8, 0.8, 0.8, 0.08)); + //g.fillAll(); + g.fillRoundedRectangle(bounds, cornerSize); + } + + //g.fillAll (button.findColour (toggleState ? DrawableButton::backgroundOnColourId + // : DrawableButton::backgroundColourId)); + + int textH = 0; + int textW = 0; + float imageratio = 0.75f; + + //if (SonoDrawableButton* const sonobutt = dynamic_cast (&button)) { + // imageratio = sonobutt->getForegroundImageRatio(); + //} + + if (button.getStyle() == DrawableButton::ImageAboveTextLabel || button.getStyle() == DrawableButton::ImageBelowTextLabel) { + textH = jmin (14, button.proportionOfHeight (0.2f)); + } else if (button.getStyle() == DrawableButton::ImageLeftOfTextLabel || button.getStyle() == DrawableButton::ImageRightOfTextLabel) { + textH = jmin (14, button.proportionOfHeight (0.8f)); + textW = jmax (20, button.proportionOfWidth (1.0f - imageratio)); + } + + + if (textH > 0) + { + g.setFont (myFont.withHeight((float) textH * fontScale)); + + g.setColour (button.findColour (toggleState ? DrawableButton::textColourOnId + : DrawableButton::textColourId) + .withMultipliedAlpha (button.isEnabled() ? 1.0f : 0.4f)); + + if (button.getStyle() == DrawableButton::ImageAboveTextLabel) { + + g.drawFittedText (button.getButtonText(), + 2, button.getHeight() - textH - 1, + button.getWidth() - 4, textH, + Justification::centred, 1); + } + else if (button.getStyle() == DrawableButton::ImageBelowTextLabel) { + g.drawFittedText (button.getButtonText(), + 2, 1, + button.getWidth() - 4, textH, + Justification::centred, 1); + } + else if (button.getStyle() == DrawableButton::ImageRightOfTextLabel) { + g.drawFittedText (button.getButtonText(), + 2, 1, + textW , button.getHeight() - 2, + Justification::centred, 2, 0.6f); + } + else if (button.getStyle() == DrawableButton::ImageLeftOfTextLabel) { + g.drawFittedText (button.getButtonText(), + button.getWidth() - textW - 4 , 1, + textW , button.getHeight() - 2, + Justification::centred, 2, 0.6f); + } + } +} + + +void CustomLookAndFeel::drawBubble (Graphics& g, BubbleComponent& comp, + const Point& tip, const Rectangle& body) +{ + Path p; + p.addBubble (body.reduced (0.5f), body.getUnion (Rectangle (tip.x, tip.y, 1.0f, 1.0f)), + tip, 5.0f, jmin (10.0f, body.getWidth() * 0.2f, body.getHeight() * 0.2f)); + + g.setColour (comp.findColour (BubbleComponent::backgroundColourId)); + g.fillPath (p); + + g.setColour (comp.findColour (BubbleComponent::outlineColourId)); + g.strokePath (p, PathStrokeType (1.0f)); +} + + +CustomBigTextLookAndFeel::CustomBigTextLookAndFeel(float maxTextSize) +: maxSize(maxTextSize) +{ + +} + +Font CustomBigTextLookAndFeel::getTextButtonFont (TextButton& button, int buttonHeight) +{ + // DBG("GetTextButton font with height: " << buttonHeight << " maxsize: " << maxSize); + float textRatio = 0.8f; + //if (SonoTextButton* const textbutt = dynamic_cast (&button)) { + // textRatio = textbutt->getTextHeightRatio(); + //} + + return myFont.withHeight(jmin (maxSize, buttonHeight * textRatio) * fontScale); +} + +Label* CustomBigTextLookAndFeel::createSliderTextBox (Slider& slider) +{ + Label * lab = LookAndFeel_V4::createSliderTextBox(slider); + lab->setKeyboardType(TextInputTarget::decimalKeyboard); + lab->setFont(myFont.withHeight(maxSize * fontScale)); + lab->setJustificationType(textJustification); + lab->setMinimumHorizontalScale(0.5); + + return lab; +} + +Button* CustomBigTextLookAndFeel::createSliderButton (Slider&, const bool isIncrement) +{ + TextButton * butt = new TextButton (isIncrement ? "+" : "-", {}); + butt->setLookAndFeel(this); + return butt; +} + + +void CustomBigTextLookAndFeel::drawToggleButton (Graphics& g, ToggleButton& button, + bool isMouseOverButton, bool isButtonDown) +{ + /* + if (button.hasKeyboardFocus (true)) + { + g.setColour (button.findColour (TextEditor::focusedOutlineColourId)); + g.drawRect (0, 0, button.getWidth(), button.getHeight()); + } + */ + + float fontSize = jmin (maxSize, button.getHeight() * 0.75f) * fontScale; + const float tickWidth = fontSize * 1.1f; + + drawTickBox (g, button, 4.0f, (button.getHeight() - tickWidth) * 0.5f, + tickWidth, tickWidth, + button.getToggleState(), + button.isEnabled(), + isMouseOverButton, + isButtonDown); + + g.setColour (button.findColour (ToggleButton::textColourId)); + g.setFont (myFont.withHeight(fontSize)); + + if (! button.isEnabled()) + g.setOpacity (0.5f); + + const int textX = (int) tickWidth + 10; + + g.drawFittedText (button.getButtonText(), + textX, 0, + button.getWidth() - textX - 2, button.getHeight(), + Justification::centredLeft, 10); +} + + diff --git a/Source/CustomLookAndFeel.h b/Source/CustomLookAndFeel.h new file mode 100644 index 0000000..dd82a3d --- /dev/null +++ b/Source/CustomLookAndFeel.h @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPLv3-or-later WITH Appstore-exception +// Copyright (C) 2020 Jesse Chappell + +#pragma once + + +#include "JuceHeader.h" + + +class CustomLookAndFeel : public LookAndFeel_V4 +{ +public: + CustomLookAndFeel(); + + void setLanguageCode(const String & lang); + + //void fillWithBackgroundTexture (Graphics&); + //static void fillWithBackgroundTexture (Component&, Graphics&); + + void setLabelCornerRadius(float val) { labelCornerRadius = val; } + float getLabelCornerRadius() const { return labelCornerRadius; } + + void drawTabButton (TabBarButton& button, Graphics&, bool isMouseOver, bool isMouseDown) override; + void drawTabbedButtonBarBackground (TabbedButtonBar&, Graphics&) override; + void drawTabAreaBehindFrontButton (TabbedButtonBar&, Graphics&, int, int) override; + void drawTabButtonText (TabBarButton& button, Graphics& g, bool isMouseOver, bool isMouseDown) override; + int getTabButtonSpaceAroundImage() override; + + int getTabButtonBestWidth (TabBarButton&, int tabDepth) override; + juce::Rectangle getTabButtonExtraComponentBounds (const TabBarButton&, juce::Rectangle& textArea, Component& extraComp) override; + + void createTabTextLayout (const TabBarButton& button, float length, float depth, + Colour colour, TextLayout& textLayout); + + //Typeface::Ptr getTypefaceForFont (const Font& font) override; + + Font getMenuBarFont (MenuBarComponent& menuBar, int /*itemIndex*/, const String& /*itemText*/) override; + + + Button* createSliderButton (Slider&, const bool isIncrement) override; + Label* createSliderTextBox (Slider&) override; + + Font getTextButtonFont (TextButton&, int buttonHeight) override; + void drawButtonText (Graphics& g, TextButton& button, bool /*isMouseOverButton*/, bool /*isButtonDown*/) override; + + void drawButtonTextWithAlignment (Graphics& g, TextButton& button, bool /*isMouseOverButton*/, bool /*isButtonDown*/, Justification textjust = Justification::centred) ; + + void drawBubble (Graphics&, BubbleComponent&, const Point& tip, const juce::Rectangle& body) override; + + + void drawFileBrowserRow (Graphics&, int width, int height, + const File& file, const String& filename, Image* icon, + const String& fileSizeDescription, const String& fileTimeDescription, + bool isDirectory, bool isItemSelected, int itemIndex, + DirectoryContentsDisplayComponent&) override; + + Button* createFileBrowserGoUpButton() override; + + void layoutFileBrowserComponent (FileBrowserComponent&, + DirectoryContentsDisplayComponent*, + FilePreviewComponent*, + ComboBox* currentPathBox, + TextEditor* filenameBox, + Button* goUpButton) override; + + + void drawTreeviewPlusMinusBox (Graphics& g, const juce::Rectangle& area, + Colour backgroundColour, bool isOpen, bool isMouseOver) override; + + + void drawToggleButton (Graphics& g, ToggleButton& button, + bool isMouseOverButton, bool isButtonDown) override; + + + void drawTickBox (Graphics&, Component&, + float x, float y, float w, float h, + bool ticked, bool isEnabled, bool isMouseOverButton, bool isButtonDown) override; + + void drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos, + const float rotaryStartAngle, const float rotaryEndAngle, Slider& slider) override; + + void drawLinearSlider (Graphics&, int x, int y, int width, int height, + float sliderPos, float minSliderPos, float maxSliderPos, + const Slider::SliderStyle, Slider&) override; + + Font getSliderPopupFont (Slider&) override; + int getSliderPopupPlacement (Slider&) override; + + int getSliderThumbRadius (Slider& slider) override; + + Slider::SliderLayout getSliderLayout (Slider& slider) override; + + void drawDrawableButton (Graphics& g, DrawableButton& button, + bool /*isMouseOverButton*/, bool /*isButtonDown*/) override; + + + void drawCallOutBoxBackground (CallOutBox& box, Graphics& g, + const Path& path, Image& cachedImage) override; + + Font getLabelFont (Label& label) override; + void drawLabel (Graphics& g, Label& label) override; + + PopupMenu::Options getOptionsForComboBoxPopupMenu (ComboBox&, Label&) override; + + Justification sliderTextJustification = Justification::centred; + + +protected: + + Font myFont; + float fontScale; + + float labelCornerRadius = 4.0f; + String languageCode; + +public: + + +}; + +class CustomBigTextLookAndFeel : public CustomLookAndFeel +{ +public: + CustomBigTextLookAndFeel(float maxTextSize=32.0f); + + Font getTextButtonFont (TextButton&, int buttonHeight) override; + Button* createSliderButton (Slider&, const bool isIncrement) override; + Label* createSliderTextBox (Slider&) override; + + void drawToggleButton (Graphics& g, ToggleButton& button, + bool isMouseOverButton, bool isButtonDown) override; + + Justification textJustification = Justification::centred; + +protected: + float maxSize; + +}; + diff --git a/Source/CustomStandaloneFilterApp.cpp b/Source/CustomStandaloneFilterApp.cpp new file mode 100644 index 0000000..2f391ad --- /dev/null +++ b/Source/CustomStandaloneFilterApp.cpp @@ -0,0 +1,219 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include "../JuceLibraryCode/JuceHeader.h" + +#include "juce_core/system/juce_TargetPlatform.h" +#include "juce_audio_plugin_client/utility/juce_CheckSettingMacros.h" + +#include "juce_audio_plugin_client/utility/juce_IncludeSystemHeaders.h" +#include "juce_audio_plugin_client/utility/juce_IncludeModuleHeaders.h" +#include "juce_audio_plugin_client/utility/juce_FakeMouseMoveGenerator.h" +#include "juce_audio_plugin_client/utility/juce_WindowsHooks.h" + +#include +#include +#include + +// #include "DebugLogC.h" + +// You can set this flag in your build if you need to specify a different +// standalone JUCEApplication class for your app to use. If you don't +// set it then by default we'll just create a simple one as below. +//#if ! JUCE_USE_CUSTOM_PLUGIN_STANDALONE_APP + +extern juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter(); + +#include "CustomStandaloneFilterWindow.h" +#include "CustomLookAndFeel.h" + +namespace juce +{ + +//============================================================================== +class StandaloneFilterApp : public JUCEApplication +{ +public: + StandaloneFilterApp() + { + PluginHostType::jucePlugInClientCurrentWrapperType = AudioProcessor::wrapperType_Standalone; + + PropertiesFile::Options options; + + options.applicationName = getApplicationName(); + options.filenameSuffix = ".settings"; + options.osxLibrarySubFolder = "Application Support/" + getApplicationName(); + #if JUCE_LINUX + options.folderName = "~/.config/paulxstretch"; + #else + options.folderName = ""; + #endif + + appProperties.setStorageParameters (options); + + LookAndFeel::setDefaultLookAndFeel(&sonoLNF); + + } + + const String getApplicationName() override { return JucePlugin_Name; } + const String getApplicationVersion() override { return JucePlugin_VersionString; } + bool moreThanOneInstanceAllowed() override { return true; } + void anotherInstanceStarted (const String&) override {} + + CustomLookAndFeel sonoLNF; + + + virtual StandaloneFilterWindow* createWindow() + { + #ifdef JucePlugin_PreferredChannelConfigurations + StandalonePluginHolder::PluginInOuts channels[] = { JucePlugin_PreferredChannelConfigurations }; + #endif + + AudioDeviceManager::AudioDeviceSetup setupOptions; + setupOptions.bufferSize = 512; + + return new StandaloneFilterWindow (getApplicationName(), + LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId), + appProperties.getUserSettings(), + false, {}, &setupOptions + #ifdef JucePlugin_PreferredChannelConfigurations + , juce::Array (channels, juce::numElementsInArray (channels)) + #else + , {} + #endif + #if JUCE_DONT_AUTO_OPEN_MIDI_DEVICES_ON_MOBILE + , false + #endif + ); + } + + //============================================================================== + void initialise (const String&) override + { + mainWindow.reset (createWindow()); + + #if JUCE_STANDALONE_FILTER_WINDOW_USE_KIOSK_MODE + Desktop::getInstance().setKioskModeComponent (mainWindow.get(), false); + #endif + + mainWindow->setVisible (true); + + Desktop::getInstance().setScreenSaverEnabled(false); + } + + void shutdown() override + { + DBG("shutdown"); + if (mainWindow.get() != nullptr) + mainWindow->pluginHolder->savePluginState(); + + mainWindow = nullptr; + appProperties.saveIfNeeded(); + } + + void suspended() override + { + DBG("suspended"); + if (mainWindow.get() != nullptr) + mainWindow->pluginHolder->savePluginState(); + + appProperties.saveIfNeeded(); + + Desktop::getInstance().setScreenSaverEnabled(true); + } + + void resumed() override + { + Desktop::getInstance().setScreenSaverEnabled(false); + } + + //============================================================================== + void systemRequestedQuit() override + { + DBG("Requested quit"); + if (mainWindow.get() != nullptr) + mainWindow->pluginHolder->savePluginState(); + + appProperties.saveIfNeeded(); + + if (ModalComponentManager::getInstance()->cancelAllModalComponents()) + { + Timer::callAfterDelay (100, []() + { + if (auto app = JUCEApplicationBase::getInstance()) + app->systemRequestedQuit(); + }); + } + else + { + quit(); + } + } + + void memoryWarningReceived() override + { + DBG("Memory warning"); + } + +protected: + ApplicationProperties appProperties; + std::unique_ptr mainWindow; +}; + +} // namespace juce + +#if JucePlugin_Build_Standalone && JUCE_IOS + +using namespace juce; + +bool JUCE_CALLTYPE juce_isInterAppAudioConnected() +{ + if (auto holder = StandalonePluginHolder::getInstance()) + return holder->isInterAppAudioConnected(); + + return false; +} + +void JUCE_CALLTYPE juce_switchToHostApplication() +{ + if (auto holder = StandalonePluginHolder::getInstance()) + holder->switchToHostApplication(); +} + +#if JUCE_MODULE_AVAILABLE_juce_gui_basics +Image JUCE_CALLTYPE juce_getIAAHostIcon (int size) +{ + if (auto holder = StandalonePluginHolder::getInstance()) + return holder->getIAAHostIcon (size); + + return Image(); +} +#endif +#endif + +START_JUCE_APPLICATION (StandaloneFilterApp); + +//#endif diff --git a/Source/CustomStandaloneFilterWindow.h b/Source/CustomStandaloneFilterWindow.h new file mode 100644 index 0000000..940d8b9 --- /dev/null +++ b/Source/CustomStandaloneFilterWindow.h @@ -0,0 +1,982 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +//#if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client +//extern juce::AudioProcessor* JUCE_API JUCE_CALLTYPE createPluginFilterOfType (juce::AudioProcessor::WrapperType type); +//#endif + +#include "CrossPlatformUtils.h" + +#include "PluginEditor.h" + +namespace juce +{ + +//============================================================================== +/** + An object that creates and plays a standalone instance of an AudioProcessor. + + The object will create your processor using the same createPluginFilter() + function that the other plugin wrappers use, and will run it through the + computer's audio/MIDI devices using AudioDeviceManager and AudioProcessorPlayer. + + @tags{Audio} +*/ +class StandalonePluginHolder : private AudioIODeviceCallback, + private Timer +{ +public: + //============================================================================== + /** Structure used for the number of inputs and outputs. */ + struct PluginInOuts { short numIns, numOuts; }; + + //============================================================================== + /** Creates an instance of the default plugin. + + The settings object can be a PropertySet that the class should use to store its + settings - the takeOwnershipOfSettings indicates whether this object will delete + the settings automatically when no longer needed. The settings can also be nullptr. + + A default device name can be passed in. + + Preferably a complete setup options object can be used, which takes precedence over + the preferredDefaultDeviceName and allows you to select the input & output device names, + sample rate, buffer size etc. + + In all instances, the settingsToUse will take precedence over the "preferred" options if not null. + */ + StandalonePluginHolder (PropertySet* settingsToUse, + bool takeOwnershipOfSettings = true, + const String& preferredDefaultDeviceName = String(), + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, + const Array& channels = Array(), + #if JUCE_ANDROID || JUCE_IOS + bool shouldAutoOpenMidiDevices = false + #else + bool shouldAutoOpenMidiDevices = false + #endif + ) + + : settings (settingsToUse, takeOwnershipOfSettings), + channelConfiguration (channels), + shouldMuteInput (! isInterAppAudioConnected()), + autoOpenMidiDevices (shouldAutoOpenMidiDevices) + { + createPlugin(); + + auto inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns + : processor->getMainBusNumInputChannels()); + + if (preferredSetupOptions != nullptr) + options.reset (new AudioDeviceManager::AudioDeviceSetup (*preferredSetupOptions)); + + auto audioInputRequired = (inChannels > 0); + + if (audioInputRequired && RuntimePermissions::isRequired (RuntimePermissions::recordAudio) + && ! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + RuntimePermissions::request (RuntimePermissions::recordAudio, + [this, preferredDefaultDeviceName] (bool granted) { init (granted, preferredDefaultDeviceName); }); + else + init (audioInputRequired, preferredDefaultDeviceName); + } + + void init (bool enableAudioInput, const String& preferredDefaultDeviceName) + { + setupAudioDevices (enableAudioInput, preferredDefaultDeviceName, options.get()); + reloadPluginState(); + startPlaying(); + + if (autoOpenMidiDevices) + startTimer (500); + } + + virtual ~StandalonePluginHolder() + { + stopTimer(); + + deletePlugin(); + shutDownAudioDevices(); + } + + //============================================================================== + virtual void createPlugin() + { + #if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client + processor.reset (::createPluginFilterOfType (AudioProcessor::wrapperType_Standalone)); + #else + AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Standalone); + processor.reset (createPluginFilter()); + AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Undefined); + #endif + jassert (processor != nullptr); // Your createPluginFilter() function must return a valid object! + + processor->disableNonMainBuses(); + processor->setRateAndBufferSizeDetails (44100, 512); + + int inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns + : processor->getMainBusNumInputChannels()); + + int outChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts + : processor->getMainBusNumOutputChannels()); + + processorHasPotentialFeedbackLoop = (inChannels > 0 && outChannels > 0); + } + + virtual void deletePlugin() + { + stopPlaying(); + processor = nullptr; + } + + static String getFilePatterns (const String& fileSuffix) + { + if (fileSuffix.isEmpty()) + return {}; + + return (fileSuffix.startsWithChar ('.') ? "*" : "*.") + fileSuffix; + } + + //============================================================================== + Value& getMuteInputValue() { return shouldMuteInput; } + bool getProcessorHasPotentialFeedbackLoop() const { return processorHasPotentialFeedbackLoop; } + + //============================================================================== + File getLastFile() const + { + File f; + + if (settings != nullptr) + f = File (settings->getValue ("lastStateFile")); + + if (f == File()) + f = File::getSpecialLocation (File::userDocumentsDirectory); + + return f; + } + + void setLastFile (const FileChooser& fc) + { + if (settings != nullptr) + settings->setValue ("lastStateFile", fc.getResult().getFullPathName()); + } + + /** Pops up a dialog letting the user save the processor's state to a file. */ + void askUserToSaveState (const String& fileSuffix = String()) + { + #if JUCE_MODAL_LOOPS_PERMITTED + FileChooser fc (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix)); + + if (fc.browseForFileToSave (true)) + { + setLastFile (fc); + + MemoryBlock data; + processor->getStateInformation (data); + + if (! fc.getResult().replaceWithData (data.getData(), data.getSize())) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + TRANS("Error whilst saving"), + TRANS("Couldn't write to the specified file!")); + } + #else + ignoreUnused (fileSuffix); + #endif + } + + /** Pops up a dialog letting the user re-load the processor's state from a file. */ + void askUserToLoadState (const String& fileSuffix = String()) + { + #if JUCE_MODAL_LOOPS_PERMITTED + FileChooser fc (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix)); + + if (fc.browseForFileToOpen()) + { + setLastFile (fc); + + MemoryBlock data; + + if (fc.getResult().loadFileAsData (data)) + processor->setStateInformation (data.getData(), (int) data.getSize()); + else + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + TRANS("Error whilst loading"), + TRANS("Couldn't read from the specified file!")); + } + #else + ignoreUnused (fileSuffix); + #endif + } + + //============================================================================== + void startPlaying() + { + player.setProcessor (processor.get()); + + #if JucePlugin_Enable_IAA && JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + { + processor->setPlayHead (device->getAudioPlayHead()); + device->setMidiMessageCollector (&player.getMidiMessageCollector()); + } + #endif + } + + void stopPlaying() + { + player.setProcessor (nullptr); + } + + //============================================================================== + /** Shows an audio properties dialog box modally. */ + void showAudioSettingsDialog(Component * calloutTarget=nullptr, Component * calloutParent=nullptr) + { + + int minNumInputs = std::numeric_limits::max(), maxNumInputs = 0, + minNumOutputs = std::numeric_limits::max(), maxNumOutputs = 0; + + auto updateMinAndMax = [] (int newValue, int& minValue, int& maxValue) + { + minValue = jmin (minValue, newValue); + maxValue = jmax (maxValue, newValue); + }; + + if (channelConfiguration.size() > 0) + { + auto defaultConfig = channelConfiguration.getReference (0); + updateMinAndMax ((int) defaultConfig.numIns, minNumInputs, maxNumInputs); + updateMinAndMax ((int) defaultConfig.numOuts, minNumOutputs, maxNumOutputs); + } + + if (auto* bus = processor->getBus (true, 0)) + updateMinAndMax (bus->getDefaultLayout().size(), minNumInputs, maxNumInputs); + + if (auto* bus = processor->getBus (false, 0)) + updateMinAndMax (bus->getDefaultLayout().size(), minNumOutputs, maxNumOutputs); + + minNumInputs = jmin (minNumInputs, maxNumInputs); + minNumOutputs = jmin (minNumOutputs, maxNumOutputs); + + auto * content = new SettingsComponent (*this, deviceManager, + minNumInputs, + maxNumInputs, + minNumOutputs, + maxNumOutputs); + if (calloutTarget && calloutParent) { + + auto wrap = std::make_unique(); + wrap->setViewedComponent(content, true); // takes ownership of content + + //std::unique_ptr contptr(content); + int defWidth = 450; + int defHeight = 550; + +#if JUCE_IOS + defWidth = 320; + defHeight = 400; +#endif + + content->setSize (defWidth, defHeight); + wrap->setSize(jmin(defWidth, calloutParent->getWidth() - 20), jmin(defHeight, calloutParent->getHeight() - 24)); + + auto bounds = calloutParent->getLocalArea(nullptr, calloutTarget->getScreenBounds()); + CallOutBox::launchAsynchronously(std::move(wrap), bounds, calloutParent); + } + else { + DialogWindow::LaunchOptions o; + + o.content.setOwned (content); + o.content->setSize (500, 550); + + o.dialogTitle = TRANS("Audio/MIDI Settings"); + o.dialogBackgroundColour = o.content->getLookAndFeel().findColour (ResizableWindow::backgroundColourId); + o.escapeKeyTriggersCloseButton = true; + o.useNativeTitleBar = true; + o.resizable = false; + + o.launchAsync(); + } + } + + void saveAudioDeviceState() + { + if (settings != nullptr) + { + std::unique_ptr xml (deviceManager.createStateXml()); + + settings->setValue ("audioSetup", xml.get()); + + #if ! (JUCE_IOS || JUCE_ANDROID) + settings->setValue ("shouldMuteInput", (bool) shouldMuteInput.getValue()); + #endif + } + } + + void reloadAudioDeviceState (bool enableAudioInput, + const String& preferredDefaultDeviceName, + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) + { + std::unique_ptr savedState; + + if (settings != nullptr) + { + savedState = settings->getXmlValue ("audioSetup"); + + #if ! (JUCE_IOS || JUCE_ANDROID) + shouldMuteInput.setValue (settings->getBoolValue ("shouldMuteInput", true)); + #endif + } + + auto totalInChannels = processor->getMainBusNumInputChannels(); + auto totalOutChannels = processor->getMainBusNumOutputChannels(); + + if (channelConfiguration.size() > 0) + { + auto defaultConfig = channelConfiguration.getReference (0); + totalInChannels = defaultConfig.numIns; + totalOutChannels = defaultConfig.numOuts; + } + + deviceManager.initialise (enableAudioInput ? totalInChannels : 0, + totalOutChannels, + savedState.get(), + true, + preferredDefaultDeviceName, + preferredSetupOptions); + } + + //============================================================================== + void savePluginState() + { + if (settings != nullptr && processor != nullptr) + { + MemoryBlock data; + processor->getStateInformation (data); + + settings->setValue ("filterState", data.toBase64Encoding()); + } + } + + void reloadPluginState() + { + if (settings != nullptr) + { + MemoryBlock data; + + if (data.fromBase64Encoding (settings->getValue ("filterState")) && data.getSize() > 0) + processor->setStateInformation (data.getData(), (int) data.getSize()); + } + } + + //============================================================================== + void switchToHostApplication() + { + #if JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + device->switchApplication(); + #endif + } + + bool isInterAppAudioConnected() + { + #if JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + return device->isInterAppAudioConnected(); + #endif + + return false; + } + + #if JUCE_MODULE_AVAILABLE_juce_gui_basics + Image getIAAHostIcon (int size) + { + #if JUCE_IOS && JucePlugin_Enable_IAA + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + return device->getIcon (size); + #else + ignoreUnused (size); + #endif + + return {}; + } + #endif + + static StandalonePluginHolder* getInstance(); + + //============================================================================== + OptionalScopedPointer settings; + std::unique_ptr processor; + AudioDeviceManager deviceManager; + AudioProcessorPlayer player; + Array channelConfiguration; + + // avoid feedback loop by default + bool processorHasPotentialFeedbackLoop = true; + Value shouldMuteInput; + AudioBuffer emptyBuffer; + bool autoOpenMidiDevices = false; + + std::unique_ptr options; + Array lastMidiDevices; + +private: + //============================================================================== + class SettingsComponent : public Component + { + public: + SettingsComponent (StandalonePluginHolder& pluginHolder, + AudioDeviceManager& deviceManagerToUse, + int minAudioInputChannels, + int maxAudioInputChannels, + int minAudioOutputChannels, + int maxAudioOutputChannels) + : owner (pluginHolder), + deviceSelector (deviceManagerToUse, + minAudioInputChannels, maxAudioInputChannels, + minAudioOutputChannels, maxAudioOutputChannels, + true, + (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), + true, false), + shouldMuteLabel ("Feedback Loop:", "Feedback Loop:"), + shouldMuteButton ("Mute audio input") + { + setOpaque (true); + + shouldMuteButton.setClickingTogglesState (true); + shouldMuteButton.getToggleStateValue().referTo (owner.shouldMuteInput); + + addAndMakeVisible (deviceSelector); + + if (owner.getProcessorHasPotentialFeedbackLoop()) + { + addAndMakeVisible (shouldMuteButton); + addAndMakeVisible (shouldMuteLabel); + + shouldMuteLabel.attachToComponent (&shouldMuteButton, true); + } + } + + void paint (Graphics& g) override + { + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); + } + + void resized() override + { + auto r = getLocalBounds(); + + if (owner.getProcessorHasPotentialFeedbackLoop()) + { + auto itemHeight = deviceSelector.getItemHeight(); + auto extra = r.removeFromTop (itemHeight); + + auto seperatorHeight = (itemHeight >> 1); + shouldMuteButton.setBounds (Rectangle (extra.proportionOfWidth (0.35f), seperatorHeight, + extra.proportionOfWidth (0.60f), deviceSelector.getItemHeight())); + + r.removeFromTop (seperatorHeight); + } + + deviceSelector.setBounds (r); + } + + private: + //============================================================================== + StandalonePluginHolder& owner; + AudioDeviceSelectorComponent deviceSelector; + Label shouldMuteLabel; + ToggleButton shouldMuteButton; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsComponent) + }; + + //============================================================================== + void audioDeviceIOCallback (const float** inputChannelData, + int numInputChannels, + float** outputChannelData, + int numOutputChannels, + int numSamples) override + { + const bool inputMuted = shouldMuteInput.getValue(); + + if (inputMuted) + { + emptyBuffer.clear(); + inputChannelData = emptyBuffer.getArrayOfReadPointers(); + } + + player.audioDeviceIOCallback (inputChannelData, numInputChannels, + outputChannelData, numOutputChannels, numSamples); + } + + void audioDeviceAboutToStart (AudioIODevice* device) override + { + emptyBuffer.setSize (device->getActiveInputChannels().countNumberOfSetBits(), device->getCurrentBufferSizeSamples()); + emptyBuffer.clear(); + + player.audioDeviceAboutToStart (device); + player.setMidiOutput (deviceManager.getDefaultMidiOutput()); + +#if JUCE_IOS + if (auto iosdevice = dynamic_cast (deviceManager.getCurrentAudioDevice())) { + processorHasPotentialFeedbackLoop = !iosdevice->isHeadphonesConnected() && device->getActiveInputChannels() > 0; + shouldMuteInput.setValue(processorHasPotentialFeedbackLoop); + } +#endif + } + + void audioDeviceStopped() override + { + player.setMidiOutput (nullptr); + player.audioDeviceStopped(); + emptyBuffer.setSize (0, 0); + } + + //============================================================================== + void setupAudioDevices (bool enableAudioInput, + const String& preferredDefaultDeviceName, + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) + { + deviceManager.addAudioCallback (this); + deviceManager.addMidiInputDeviceCallback ({}, &player); + + reloadAudioDeviceState (enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions); + } + + void shutDownAudioDevices() + { + saveAudioDeviceState(); + + deviceManager.removeMidiInputDeviceCallback ({}, &player); + deviceManager.removeAudioCallback (this); + } + + void timerCallback() override + { + auto newMidiDevices = MidiInput::getAvailableDevices(); + + if (newMidiDevices != lastMidiDevices) + { + for (auto& oldDevice : lastMidiDevices) + if (! newMidiDevices.contains (oldDevice)) + deviceManager.setMidiInputDeviceEnabled (oldDevice.identifier, false); + + for (auto& newDevice : newMidiDevices) + if (! lastMidiDevices.contains (newDevice)) + deviceManager.setMidiInputDeviceEnabled (newDevice.identifier, true); + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandalonePluginHolder) +}; + +//============================================================================== +/** + A class that can be used to run a simple standalone application containing your filter. + + Just create one of these objects in your JUCEApplicationBase::initialise() method, and + let it do its work. It will create your filter object using the same createPluginFilter() function + that the other plugin wrappers use. + + @tags{Audio} +*/ +class StandaloneFilterWindow : public DocumentWindow, + public Button::Listener +{ +public: + //============================================================================== + typedef StandalonePluginHolder::PluginInOuts PluginInOuts; + + //============================================================================== + /** Creates a window with a given title and colour. + The settings object can be a PropertySet that the class should use to + store its settings (it can also be null). If takeOwnershipOfSettings is + true, then the settings object will be owned and deleted by this object. + */ + StandaloneFilterWindow (const String& title, + Colour backgroundColour, + PropertySet* settingsToUse, + bool takeOwnershipOfSettings, + const String& preferredDefaultDeviceName = String(), + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, + const Array& constrainToConfiguration = {}, + #if JUCE_ANDROID || JUCE_IOS + bool autoOpenMidiDevices = false + #else + bool autoOpenMidiDevices = false + #endif + ) + : DocumentWindow (title, backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton), + optionsButton ("Options") + { + + + #if JUCE_IOS || JUCE_ANDROID + setTitleBarHeight (0); + #else + setTitleBarButtonsRequired (DocumentWindow::minimiseButton | DocumentWindow::closeButton, false); + + Component::addAndMakeVisible (optionsButton); + optionsButton.addListener (this); + optionsButton.setTriggeredOnMouseDown (true); + setUsingNativeTitleBar(true); + #endif + + setResizable (true, false); + + pluginHolder.reset (new StandalonePluginHolder (settingsToUse, takeOwnershipOfSettings, + preferredDefaultDeviceName, preferredSetupOptions, + constrainToConfiguration, autoOpenMidiDevices)); + + #if JUCE_IOS || JUCE_ANDROID + setFullScreen (true); + setContentOwned (new MainContentComponent (*this), false); + #else + setContentOwned (new MainContentComponent (*this), true); + + if (auto* props = pluginHolder->settings.get()) + { + const int x = props->getIntValue ("windowX", -100); + const int y = props->getIntValue ("windowY", -100); + + if (x != -100 && y != -100) + setBoundsConstrained ({ x, y, getWidth(), getHeight() }); + else + centreWithSize (getWidth(), getHeight()); + } + else + { + centreWithSize (getWidth(), getHeight()); + } + #endif + } + + ~StandaloneFilterWindow() + { + #if (! JUCE_IOS) && (! JUCE_ANDROID) + if (auto* props = pluginHolder->settings.get()) + { + props->setValue ("windowX", getX()); + props->setValue ("windowY", getY()); + } + #endif + + pluginHolder->stopPlaying(); + clearContentComponent(); + pluginHolder = nullptr; + } + + //============================================================================== + AudioProcessor* getAudioProcessor() const noexcept { return pluginHolder->processor.get(); } + AudioDeviceManager& getDeviceManager() const noexcept { return pluginHolder->deviceManager; } + + /** Deletes and re-creates the plugin, resetting it to its default state. */ + void resetToDefaultState() + { + pluginHolder->stopPlaying(); + clearContentComponent(); + pluginHolder->deletePlugin(); + + if (auto* props = pluginHolder->settings.get()) + props->removeValue ("filterState"); + + pluginHolder->createPlugin(); + setContentOwned (new MainContentComponent (*this), true); + pluginHolder->startPlaying(); + } + + //============================================================================== + void closeButtonPressed() override + { + pluginHolder->savePluginState(); + + JUCEApplicationBase::quit(); + } + + void buttonClicked (Button*) override + { + PopupMenu m; + m.addItem (1, TRANS("Audio/MIDI Settings...")); + m.addSeparator(); + m.addItem (2, TRANS("Save current state...")); + m.addItem (3, TRANS("Load a saved state...")); + m.addSeparator(); + m.addItem (4, TRANS("Reset to default state")); + + m.showMenuAsync (PopupMenu::Options(), + ModalCallbackFunction::forComponent (menuCallback, this)); + } + + void handleMenuResult (int result) + { + switch (result) + { + case 1: pluginHolder->showAudioSettingsDialog(); break; + case 2: pluginHolder->askUserToSaveState(); break; + case 3: pluginHolder->askUserToLoadState(); break; + case 4: resetToDefaultState(); break; + default: break; + } + } + + static void menuCallback (int result, StandaloneFilterWindow* button) + { + if (button != nullptr && result != 0) + button->handleMenuResult (result); + } + + void resized() override + { + DocumentWindow::resized(); + optionsButton.setBounds (8, 6, 60, getTitleBarHeight() - 8); + } + + virtual StandalonePluginHolder* getPluginHolder() { return pluginHolder.get(); } + + std::unique_ptr pluginHolder; + + +private: + //============================================================================== + class MainContentComponent : public Component, + private Value::Listener, + private Button::Listener, + private ComponentListener + { + public: + MainContentComponent (StandaloneFilterWindow& filterWindow) + : owner (filterWindow), notification (this), + editor (owner.getAudioProcessor()->createEditorIfNeeded()) + { + // because the plugin editor may have changed this + //filterWindow.setBackgroundColour(LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId)); + filterWindow.setBackgroundColour(Colours::black); + + Value& inputMutedValue = owner.pluginHolder->getMuteInputValue(); + + if (editor != nullptr) + { + // hack to allow editor to get devicemanager + if (auto * sonoeditor = dynamic_cast(editor.get())) { + sonoeditor->getAudioDeviceManager = [this]() { return &owner.getDeviceManager(); }; + sonoeditor->showAudioSettingsDialog = [this](Component* calloutTarget, Component* calloutParent) { owner.pluginHolder->showAudioSettingsDialog(calloutTarget, calloutParent); }; + } + + editor->addComponentListener (this); + componentMovedOrResized (*editor, false, true); + + addAndMakeVisible (editor.get()); + } + + addChildComponent (notification); + + if (owner.pluginHolder->getProcessorHasPotentialFeedbackLoop()) + { + inputMutedValue.addListener (this); + shouldShowNotification = inputMutedValue.getValue(); + } + + inputMutedChanged (shouldShowNotification); + } + + ~MainContentComponent() + { + if (editor != nullptr) + { + editor->removeComponentListener (this); + owner.pluginHolder->processor->editorBeingDeleted (editor.get()); + editor = nullptr; + } + } + + void resized() override + { + auto r = getLocalBounds(); + + bool portrait = getWidth() < getHeight(); + bool tall = getHeight() > 500; + + float safetop=0.0f, safebottom=0.0f, safeleft=0.0f, saferight=0.0f; + int notchPos = 0; + getSafeAreaInsets(getWindowHandle(), safetop, safebottom, safeleft, saferight, notchPos); + + if (portrait != isPortrait || isTall != tall || orientation != notchPos) { + isPortrait = portrait; + isTall = tall; + orientation = notchPos; + + // call resized again if on iOS, due to dumb stuff related to safe area insets not being updated +#if JUCE_IOS + Timer::callAfterDelay(150, [this]() { + this->resized(); + }); + //return; +#endif + } + + topInset = safetop; + bottomInset = safebottom; + leftInset = safeleft * (notchPos == 3 ? 0.75f : 0.5f); + rightInset = saferight * (notchPos == 4 ? 0.75f : 0.5f); + + r.removeFromTop(topInset); + r.removeFromBottom(bottomInset); + r.removeFromLeft(leftInset); + r.removeFromRight(rightInset); + + + if (shouldShowNotification) { + notification.setBounds (r.removeFromTop (NotificationArea::height)); + topInset += NotificationArea::height; + } + + editor->setBounds (r); + } + + private: + + bool isPortrait = false; + bool isTall = false; + int orientation = 0; + + //============================================================================== + class NotificationArea : public Component + { + public: + enum { height = 60 }; + + NotificationArea (Button::Listener* settingsButtonListener) + : notification ("notification", "Audio input is muted to avoid\nfeedback loop.\nHeadphones recommended!"), + #if JUCE_IOS || JUCE_ANDROID + settingsButton ("Unmute Input") + #else + settingsButton ("Settings...") + #endif + { + setOpaque (true); + + notification.setColour (Label::textColourId, Colours::black); + + settingsButton.addListener (settingsButtonListener); + + addAndMakeVisible (notification); + addAndMakeVisible (settingsButton); + } + + void paint (Graphics& g) override + { + auto r = getLocalBounds(); + + g.setColour (Colours::darkgoldenrod); + g.fillRect (r.removeFromBottom (1)); + + g.setColour (Colours::lightgoldenrodyellow); + g.fillRect (r); + } + + void resized() override + { + auto r = getLocalBounds().reduced (5); + + settingsButton.setBounds (r.removeFromRight (70)); + notification.setBounds (r); + } + private: + Label notification; + TextButton settingsButton; + }; + + //============================================================================== + void inputMutedChanged (bool newInputMutedValue) + { + shouldShowNotification = newInputMutedValue; + notification.setVisible (shouldShowNotification); + + #if JUCE_IOS || JUCE_ANDROID + resized(); + #else + setSize (editor->getWidth(), + editor->getHeight() + + (shouldShowNotification ? NotificationArea::height : 0)); + #endif + } + + void valueChanged (Value& value) override { inputMutedChanged (value.getValue()); } + void buttonClicked (Button*) override + { + #if JUCE_IOS || JUCE_ANDROID + owner.pluginHolder->getMuteInputValue().setValue (false); + #else + owner.pluginHolder->showAudioSettingsDialog(); + #endif + } + + //============================================================================== + void componentMovedOrResized (Component&, bool, bool wasResized) override + { + if (wasResized && editor != nullptr) + setSize (editor->getWidth() + leftInset + rightInset, + editor->getHeight() + topInset + bottomInset); + } + + //============================================================================== + StandaloneFilterWindow& owner; + NotificationArea notification; + std::unique_ptr editor; + bool shouldShowNotification = false; + + int topInset = 0; + int bottomInset = 0; + int leftInset = 0; + int rightInset = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) + }; + + //============================================================================== + TextButton optionsButton; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandaloneFilterWindow) +}; + +inline StandalonePluginHolder* StandalonePluginHolder::getInstance() +{ + #if JucePlugin_Enable_IAA || JucePlugin_Build_Standalone + if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone) + { + auto& desktop = Desktop::getInstance(); + const int numTopLevelWindows = desktop.getNumComponents(); + + for (int i = 0; i < numTopLevelWindows; ++i) + if (auto window = dynamic_cast (desktop.getComponent (i))) + return window->getPluginHolder(); + } + #endif + + return nullptr; +} + +} // namespace juce diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index e63507d..51b54ec 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -28,6 +28,19 @@ static void handleSettingsMenuModalCallback(int choice, PaulstretchpluginAudioPr ed->executeModalMenuAction(0,choice); } +enum ParameterGroupIds +{ + HarmonicsGroup = 0, + TonalNoiseGroup = 1, + FrequencyShiftGroup = 2, + PitchShiftGroup = 3, + RatiosGroup = 4, + FrequencySpreadGroup = 5, + FilterGroup = 6, + FreeFilterGroup = 7, + CompressGroup = 8 +}; + //============================================================================== PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(PaulstretchpluginAudioProcessor& p) : AudioProcessorEditor(&p), @@ -37,7 +50,8 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau m_wavefilter_tab(p.m_cur_tab_index), m_filefilter(p.m_afm->getWildcardForAllFormats(),String(),String()) { - + LookAndFeel::setDefaultLookAndFeel(&m_lookandfeel); + setLookAndFeel(&m_lookandfeel); setWantsKeyboardFocus(true); @@ -52,8 +66,14 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau { return jmap(x, 0.0, 1.0, -48.0, 12.0); }; - m_wavefilter_tab.setTabBarDepth(20); - + int tabdepth = 24; + +#if JUCE_IOS + tabdepth = 36; +#endif + m_wavefilter_tab.setTabBarDepth(tabdepth); + m_wavefilter_tab.getTabbedButtonBar().setMinimumTabScaleFactor(0.25f); + addAndMakeVisible(&m_perfmeter); addAndMakeVisible(&m_import_button); @@ -67,7 +87,7 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau m_settings_button.setButtonText("Settings..."); m_settings_button.onClick = [this]() { showSettingsMenu(); }; - if (processor.wrapperType == AudioProcessor::wrapperType_Standalone) + if (JUCEApplicationBase::isStandaloneApp()) { addAndMakeVisible(&m_render_button); m_render_button.setButtonText("Render..."); @@ -98,7 +118,7 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau notifyonlyonrelease = true; int group_id = -1; if (i == cpi_harmonicsbw || i == cpi_harmonicsfreq || i == cpi_harmonicsgauss || i == cpi_numharmonics) - group_id = 0; + group_id = HarmonicsGroup; if (i == cpi_octavesm2 || i == cpi_octavesm1 || i == cpi_octaves0 || i == cpi_octaves1 || i == cpi_octaves15 || i == cpi_octaves2 || i==cpi_octaves_extra1 || i==cpi_octaves_extra2) group_id = -2; // -2 for not included in the main parameters page @@ -107,17 +127,17 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau if ((i >= cpi_enable_spec_module0 && i <= cpi_enable_spec_module8)) group_id = -2; if (i == cpi_tonalvsnoisebw || i == cpi_tonalvsnoisepreserve) - group_id = 1; + group_id = TonalNoiseGroup; if (i == cpi_filter_low || i == cpi_filter_high) - group_id = 6; + group_id = FilterGroup; if (i == cpi_compress) - group_id = 8; + group_id = CompressGroup; if (i == cpi_spreadamount) - group_id = 5; + group_id = FrequencySpreadGroup; if (i == cpi_frequencyshift) - group_id = 2; + group_id = FrequencyShiftGroup; if (i == cpi_pitchshift) - group_id = 3; + group_id = PitchShiftGroup; if (i == cpi_freefilter_scaley || i == cpi_freefilter_shiftx || i == cpi_freefilter_shifty || i == cpi_freefilter_tilty || i == cpi_freefilter_randomy_amount || i == cpi_freefilter_randomy_numbands || i == cpi_freefilter_randomy_rate) @@ -126,14 +146,107 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau { m_parcomps.emplace_back(std::make_unique(pars[i], notifyonlyonrelease)); m_parcomps.back()->m_group_id = group_id; - if (group_id >= -1) - addAndMakeVisible(m_parcomps.back().get()); + + if (group_id == -1) // only add ones that aren't in groups + addAndMakeVisible(m_parcomps.back().get()); } else { m_parcomps.push_back(nullptr); } } + + m_parcomps[cpi_num_inchans]->getSlider()->setSliderStyle(Slider::SliderStyle::IncDecButtons); + m_parcomps[cpi_num_inchans]->getSlider()->setTextBoxStyle(Slider::TextEntryBoxPosition::TextBoxLeft, false, 30, 34); + m_parcomps[cpi_num_outchans]->getSlider()->setSliderStyle(Slider::SliderStyle::IncDecButtons); + m_parcomps[cpi_num_outchans]->getSlider()->setTextBoxStyle(Slider::TextEntryBoxPosition::TextBoxLeft, false, 30, 34); + + m_groupviewport = std::make_unique(); + m_groupcontainer = std::make_unique(); + m_groupviewport->setViewedComponent(m_groupcontainer.get(), false); + + addAndMakeVisible(m_groupviewport.get()); + + m_stretchgroup = std::make_unique("", -1, &processor, false); + m_stretchgroup->setBackgroundColor(Colour(0xff332244)); + m_stretchgroup->addParameterComponent(m_parcomps[cpi_stretchamount].get()); + m_stretchgroup->addParameterComponent(m_parcomps[cpi_fftsize].get()); + + addAndMakeVisible(m_stretchgroup.get()); + + + m_posgroup = std::make_unique("", -1, &processor, false); + m_posgroup->addParameterComponent(m_parcomps[cpi_loopxfadelen].get()); + m_posgroup->addParameterComponent(m_parcomps[cpi_onsetdetection].get()); + m_posgroup->addParameterComponent(m_parcomps[cpi_soundstart].get()); + m_posgroup->addParameterComponent(m_parcomps[cpi_soundend].get()); + + m_groupcontainer->addAndMakeVisible(m_posgroup.get()); + + + auto harmgroup = std::make_unique("", HarmonicsGroup, &processor); + harmgroup->addParameterComponent(m_parcomps[cpi_numharmonics].get()); + harmgroup->addParameterComponent(m_parcomps[cpi_harmonicsfreq].get()); + harmgroup->addParameterComponent(m_parcomps[cpi_harmonicsbw].get()); + harmgroup->addParameterComponent(m_parcomps[cpi_harmonicsgauss].get()); + harmgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + + m_groupcontainer->addAndMakeVisible(harmgroup.get()); + m_pargroups.insert( {HarmonicsGroup, std::move(harmgroup) }); + + auto tonegroup = std::make_unique("", TonalNoiseGroup, &processor); + tonegroup->addParameterComponent(m_parcomps[cpi_tonalvsnoisebw].get()); + tonegroup->addParameterComponent(m_parcomps[cpi_tonalvsnoisepreserve].get()); + tonegroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(tonegroup.get()); + m_pargroups.insert( {TonalNoiseGroup, std::move(tonegroup) }); + + auto fsgroup = std::make_unique("", FrequencyShiftGroup, &processor); + fsgroup->addParameterComponent(m_parcomps[cpi_frequencyshift].get()); + fsgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(fsgroup.get()); + m_pargroups.insert( {FrequencyShiftGroup, std::move(fsgroup) }); + + auto psgroup = std::make_unique("", PitchShiftGroup, &processor); + psgroup->addParameterComponent(m_parcomps[cpi_pitchshift].get()); + psgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(psgroup.get()); + m_pargroups.insert( {PitchShiftGroup, std::move(psgroup) }); + + auto spreadgroup = std::make_unique("", FrequencySpreadGroup, &processor); + spreadgroup->addParameterComponent(m_parcomps[cpi_spreadamount].get()); + spreadgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(spreadgroup.get()); + m_pargroups.insert( {FrequencySpreadGroup, std::move(spreadgroup) }); + + auto filtgroup = std::make_unique("", FilterGroup, &processor); + filtgroup->addParameterComponent(m_parcomps[cpi_filter_low].get()); + filtgroup->addParameterComponent(m_parcomps[cpi_filter_high].get()); + filtgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(filtgroup.get()); + m_pargroups.insert( {FilterGroup, std::move(filtgroup) }); + + auto compgroup = std::make_unique("", CompressGroup, &processor); + compgroup->addParameterComponent(m_parcomps[cpi_compress].get()); + compgroup->EnabledChangedCallback = [this]() { + processor.setDirty(); + }; + m_groupcontainer->addAndMakeVisible(compgroup.get()); + m_pargroups.insert( {CompressGroup, std::move(compgroup) }); + + //m_parcomps[cpi_dryplayrate]->getSlider()->setSkewFactorFromMidPoint(1.0); //addAndMakeVisible(&m_specvis); m_wave_container->addAndMakeVisible(&m_zs); @@ -165,13 +278,25 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau m_spec_order_ed.setSource(processor.getStretchSource()); addAndMakeVisible(&m_spec_order_ed); + + m_spec_order_ed.ModuleSelectedCallback = [this](int id) { + if (id == FreeFilterGroup) { + if (isSpectrumProcGroupEnabled(id)) { + m_wavefilter_tab.setCurrentTabIndex(2); + } + } else if (id == RatiosGroup) { + if (isSpectrumProcGroupEnabled(id)) { + m_wavefilter_tab.setCurrentTabIndex(1); + } + } + for (int i = 0; i < m_parcomps.size(); ++i) { if (m_parcomps[i] != nullptr) { - if (m_parcomps[i]->m_group_id == id) + if (m_parcomps[i]->m_group_id == id) m_parcomps[i]->setHighLighted(true); else m_parcomps[i]->setHighLighted(false); @@ -233,9 +358,12 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau *processor.getFloatParameter((int)cpi_octaves_ratio0 + index) = val; }; m_wave_container->addAndMakeVisible(&m_wavecomponent); - m_wavefilter_tab.addTab("Waveform", Colours::white, m_wave_container, true); - m_wavefilter_tab.addTab("Ratio mixer", Colours::white, &m_ratiomixeditor, false); - m_wavefilter_tab.addTab("Free filter", Colours::white, &m_free_filter_component, false); + + auto tabbgcol = Colour(0xff555555); + + m_wavefilter_tab.addTab("Waveform", tabbgcol, m_wave_container, true); + m_wavefilter_tab.addTab("Ratio mixer", tabbgcol, &m_ratiomixeditor, false); + m_wavefilter_tab.addTab("Free filter", tabbgcol, &m_free_filter_component, false); //m_wavefilter_tab.addTab("Spectrum", Colours::white, &m_sonogram, false); addAndMakeVisible(&m_wavefilter_tab); @@ -246,7 +374,7 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau m_wavecomponent.startTimer(100); - setResizeLimits(320, 14*25 + 120, 40000, 4000); + setResizeLimits(320, 570, 40000, 4000); setResizable(true, !JUCEApplicationBase::isStandaloneApp()); @@ -257,9 +385,23 @@ PaulstretchpluginAudioProcessorEditor::PaulstretchpluginAudioProcessorEditor(Pau PaulstretchpluginAudioProcessorEditor::~PaulstretchpluginAudioProcessorEditor() { + LookAndFeel::setDefaultLookAndFeel(nullptr); + setLookAndFeel(nullptr); //Logger::writeToLog("PaulX Editor destroyed"); } +bool PaulstretchpluginAudioProcessorEditor::isSpectrumProcGroupEnabled(int groupid) +{ + auto order = processor.getStretchSource()->getSpectrumProcessOrder(); + for (int i=0; i < order.size(); ++i) { + if (order[i].m_index == groupid) { + return order[i].m_enabled->get(); + } + } + return false; +} + + void PaulstretchpluginAudioProcessorEditor::showRenderDialog() { auto contentraw = new RenderSettingsComponent(&processor); @@ -268,6 +410,13 @@ void PaulstretchpluginAudioProcessorEditor::showRenderDialog() CallOutBox::launchAsynchronously(std::move(content), m_render_button.getBounds(), this); } +void PaulstretchpluginAudioProcessorEditor::showAudioSetup() +{ + if (showAudioSettingsDialog) { + showAudioSettingsDialog(&m_settings_button, this); + } +} + void PaulstretchpluginAudioProcessorEditor::executeModalMenuAction(int menuid, int r) { if (r >= 200 && r < 210) @@ -275,40 +424,44 @@ void PaulstretchpluginAudioProcessorEditor::executeModalMenuAction(int menuid, i int caplen = m_capturelens[r - 200]; *processor.getFloatParameter(cpi_max_capture_len) = (float)caplen; } - if (r == 1) + else if (r == 1) { toggleBool(processor.m_play_when_host_plays); } - if (r == 2) + else if (r == 2) { toggleBool(processor.m_capture_when_host_plays); } - if (r == 8) + else if (r == 8) { toggleBool(processor.m_mute_while_capturing); } - if (r == 10) + else if (r == 10) { toggleBool(processor.m_mute_processed_while_capturing); } - if (r == 4) + else if (r == 4) { processor.resetParameters(); } - if (r == 5) + else if (r == 5) { toggleBool(processor.m_load_file_with_state); } - if (r == 9) + else if (r == 9) { toggleBool(processor.m_save_captured_audio); } - if (r == 3) + else if (r == 3) { showAbout(); } + else if (r == 11) + { + showAudioSetup(); + } - if (r == 6) + else if (r == 6) { ValueTree tree = processor.getStateTree(true, true); MemoryBlock destData; @@ -317,7 +470,7 @@ void PaulstretchpluginAudioProcessorEditor::executeModalMenuAction(int menuid, i String txt = Base64::toBase64(destData.getData(), destData.getSize()); SystemClipboard::copyTextToClipboard(txt); } - if (r == 7) + else if (r == 7) { toggleBool(processor.m_show_technical_info); processor.m_propsfile->m_props_file->setValue("showtechnicalinfo", processor.m_show_technical_info); @@ -328,119 +481,254 @@ void PaulstretchpluginAudioProcessorEditor::executeModalMenuAction(int menuid, i void PaulstretchpluginAudioProcessorEditor::paint (Graphics& g) { - g.fillAll(Colours::darkgrey); + g.fillAll(Colour(0xff404040)); } void PaulstretchpluginAudioProcessorEditor::resized() { - m_import_button.setBounds(1, 1, 60, 24); - m_import_button.changeWidthToFitText(); - m_settings_button.setBounds(m_import_button.getRight() + 1, 1, 60, 24); - m_settings_button.changeWidthToFitText(); - int yoffs = m_settings_button.getRight() + 1; - if (processor.wrapperType == AudioProcessor::wrapperType_Standalone) - { - m_render_button.setBounds(yoffs, 1, 60, 24); - m_render_button.changeWidthToFitText(); - yoffs = m_render_button.getRight() + 1; - } - m_rewind_button.setBounds(yoffs, 1, 30, 24); - yoffs = m_rewind_button.getRight() + 1; - m_perfmeter.setBounds(yoffs, 1, 150, 24); - m_info_label.setBounds(m_perfmeter.getRight() + 1, m_settings_button.getY(), - getWidth()- m_perfmeter.getRight()-1, 24); - int w = getWidth(); - int xoffs = 1; - yoffs = 30; - int div = w / 6; - //std::vector> layout; - //layout.emplace_back(cpi_capture_enabled, cpi_passthrough, cpi_pause_enabled, cpi_freeze); - //layout.emplace_back(cpi_main_volume, cpi_num_inchans, cpi_num_outchans); - m_parcomps[cpi_capture_trigger]->setBounds(xoffs, yoffs, div-1, 24); - //xoffs += div; - //m_parcomps[cpi_max_capture_len]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_passthrough]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_pause_enabled]->setBounds(xoffs, yoffs, div-1, 24); - xoffs += div; - m_parcomps[cpi_freeze]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_bypass_stretch]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_looping_enabled]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - div = w / 3; - m_parcomps[cpi_main_volume]->setBounds(xoffs, yoffs, div-1, 24); - xoffs += div; - m_parcomps[cpi_num_inchans]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_num_outchans]->setBounds(xoffs, yoffs, div-1, 24); - div = w / 2; - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_fftsize]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_stretchamount]->setBounds(xoffs, yoffs, div - 1, 24); - m_parcomps[cpi_dryplayrate]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_pitchshift]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_frequencyshift]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_numharmonics]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_harmonicsfreq]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_harmonicsbw]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_harmonicsgauss]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_spreadamount]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_compress]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_tonalvsnoisebw]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_tonalvsnoisepreserve]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - // filter here - m_parcomps[cpi_filter_low]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_filter_high]->setBounds(xoffs, yoffs, div - 1, 24); - - xoffs = 1; - yoffs += 25; - - m_parcomps[cpi_loopxfadelen]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_onsetdetection]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs = 1; - yoffs += 25; - m_parcomps[cpi_soundstart]->setBounds(xoffs, yoffs, div - 1, 24); - xoffs += div; - m_parcomps[cpi_soundend]->setBounds(xoffs, yoffs, div - 1, 24); - //yoffs += 25; - //xoffs = 1; - - yoffs += 25; - int remain_h = getHeight() - 1 - yoffs; - m_spec_order_ed.setBounds(1, yoffs, getWidth() - 2, remain_h / 9 * 1); - m_wavefilter_tab.setBounds(1, m_spec_order_ed.getBottom() + 1, getWidth() - 2, remain_h / 9 * 8); - m_wavecomponent.setBounds(m_wave_container->getX(), 0, m_wave_container->getWidth(), + auto bounds = getLocalBounds(); + + bounds.reduce(4, 0); + bounds.removeFromRight(4); + + int w = bounds.getWidth(); + int rowheight = 24; + int togglerowheight = 24; + int buttonrowheight = 32; + int minw = w / 3; + int toggleminw = 90; + int minh = 32; + int buttw = 60; + int buttminw = 36; + int minitemw = 300; + int margin = 2; + +#if JUCE_IOS + togglerowheight = 32; + rowheight = 36; + buttonrowheight = 40; + minh = 40; +#endif + + FlexBox mainbox; + mainbox.flexDirection = FlexBox::Direction::column; + + + FlexBox topbox; + topbox.flexDirection = FlexBox::Direction::row; + topbox.flexWrap = FlexBox::Wrap::wrap; + topbox.alignContent = FlexBox::AlignContent::flexStart; + + topbox.items.add(FlexItem(buttw, buttonrowheight, m_import_button).withMargin(1).withFlex(1).withMaxWidth(130)); + topbox.items.add(FlexItem(buttw, buttonrowheight, m_settings_button).withMargin(1).withFlex(1).withMaxWidth(130)); + if (JUCEApplicationBase::isStandaloneApp()) { + topbox.items.add(FlexItem(buttw, buttonrowheight, m_render_button).withMargin(1).withFlex(1).withMaxWidth(130)); + } + topbox.items.add(FlexItem(buttminw, buttonrowheight, m_rewind_button).withMargin(1)); + topbox.items.add(FlexItem(4, 4)); + topbox.items.add(FlexItem(80, buttonrowheight, m_perfmeter).withMargin(1).withFlex(1).withMaxWidth(110).withMaxHeight(24).withAlignSelf(FlexItem::AlignSelf::center)); + topbox.items.add(FlexItem(140, 26, m_info_label).withMargin(1).withFlex(2)); + + topbox.performLayout(Rectangle(0,0,w - 2*margin,400)); // test run to calculate actual used height + int topboxh = topbox.items.getLast().currentBounds.getBottom() + + topbox.items.getLast().margin.bottom; + + + FlexBox togglesbox; + togglesbox.flexDirection = FlexBox::Direction::row; + togglesbox.flexWrap = FlexBox::Wrap::wrap; + togglesbox.alignContent = FlexBox::AlignContent::flexStart; + + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_capture_trigger]).withMargin(margin).withFlex(1).withMaxWidth(200)); + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_passthrough]).withMargin(margin).withFlex(1.5).withMaxWidth(200)); + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_pause_enabled]).withMargin(margin).withFlex(1).withMaxWidth(200)); + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_freeze]).withMargin(margin).withFlex(1).withMaxWidth(200)); + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_bypass_stretch]).withMargin(margin).withFlex(1).withMaxWidth(200)); + togglesbox.items.add(FlexItem(toggleminw, togglerowheight, *m_parcomps[cpi_looping_enabled]).withMargin(margin).withFlex(1).withMaxWidth(200)); + + togglesbox.performLayout(Rectangle(0,0,w - 2*margin,400)); // test run to calculate actual used height + int toggleh = togglesbox.items.getLast().currentBounds.getBottom() + togglesbox.items.getLast().margin.bottom; + DBG("toggle h: " << toggleh); + + FlexBox volbox; + volbox.flexDirection = FlexBox::Direction::row; + volbox.flexWrap = FlexBox::Wrap::wrap; + volbox.alignContent = FlexBox::AlignContent::flexStart; + volbox.items.add(FlexItem(minitemw*0.75f, rowheight, *m_parcomps[cpi_main_volume]).withMargin(margin).withFlex(1)); + + FlexBox inoutbox; + int inoutminw = 170; + int inoutmaxw = 200; + + inoutbox.flexDirection = FlexBox::Direction::row; + inoutbox.items.add(FlexItem(inoutminw, rowheight, *m_parcomps[cpi_num_inchans]).withMargin(margin).withFlex(0.5).withMaxWidth(inoutmaxw)); + inoutbox.items.add(FlexItem(inoutminw, rowheight, *m_parcomps[cpi_num_outchans]).withMargin(margin).withFlex(0.5).withMaxWidth(inoutmaxw)); + + volbox.items.add(FlexItem(2*inoutminw, rowheight, inoutbox).withMargin(margin).withFlex(1.5).withMaxWidth(2*inoutmaxw + 10)); + + volbox.performLayout(Rectangle(0,0,w - 2*margin,400)); // test run to calculate actual used height + int volh = volbox.items.getLast().currentBounds.getBottom() + volbox.items.getLast().margin.bottom; + int stretchH = m_stretchgroup->getMinimumHeight(w - 2*margin); + + + + FlexBox groupsbox; + groupsbox.flexDirection = FlexBox::Direction::column; + + int scrollw = m_groupviewport->getScrollBarThickness() ; + + int gheight = 0; + int groupmargin = 2; + int groupw = w - 2*groupmargin - scrollw; + // groups + + minh = m_posgroup->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_posgroup).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + + + minh = m_pargroups[HarmonicsGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[HarmonicsGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + minh = m_pargroups[TonalNoiseGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[TonalNoiseGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + FlexBox shiftbox; + FlexBox scompbox; + + if (w >= 700) { + shiftbox.flexDirection = FlexBox::Direction::row; + minh = m_pargroups[FrequencyShiftGroup]->getMinimumHeight(minw); + shiftbox.items.add(FlexItem(minw, minh, *m_pargroups[FrequencyShiftGroup]).withFlex(1)); + shiftbox.items.add(FlexItem(4, minh)); + shiftbox.items.add(FlexItem(minw, minh, *m_pargroups[PitchShiftGroup]).withFlex(1)); + groupsbox.items.add(FlexItem(minw, minh, shiftbox).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + scompbox.flexDirection = FlexBox::Direction::row; + minh = m_pargroups[FrequencySpreadGroup]->getMinimumHeight(minw); + scompbox.items.add(FlexItem(minw, minh, *m_pargroups[FrequencySpreadGroup]).withFlex(1)); + scompbox.items.add(FlexItem(4, minh)); + scompbox.items.add(FlexItem(minw, minh, *m_pargroups[CompressGroup]).withFlex(1)); + groupsbox.items.add(FlexItem(minw, minh, scompbox).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + } else { + minh = m_pargroups[FrequencyShiftGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[FrequencyShiftGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + minh = m_pargroups[PitchShiftGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[PitchShiftGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + minh = m_pargroups[FrequencySpreadGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[FrequencySpreadGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + minh = m_pargroups[CompressGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[CompressGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + } + + minh = m_pargroups[FilterGroup]->getMinimumHeight(groupw); + groupsbox.items.add(FlexItem(minw, minh, *m_pargroups[FilterGroup]).withMargin(groupmargin)); + gheight += minh + 2*groupmargin; + + /* + for (const auto & group : m_pargroups) { + int minheight = group.second->getMinimumHeight(w); + groupsbox.items.add(FlexItem(minw, minheight, *(group.second)).withMargin(2)); + gheight += minheight + 4; + } + */ + + DBG("group tot height: " << gheight); + + int useh = gheight; + int vpminh = jmin(useh, 140); + int tabminh = 200; + int orderminh = 34; + +#if JUCE_IOS + tabminh = 234; +#endif + + int totminh = vpminh + orderminh + tabminh + topboxh + toggleh + volh + stretchH; + + mainbox.items.add(FlexItem(minw, topboxh, topbox).withMargin(margin).withFlex(0)); + + + + auto reparentIfNecessary = [] (Component * comp, Component *newparent) { + if (comp->getParentComponent() != newparent) + newparent->addAndMakeVisible(comp); + }; + + std::function reparentItemsIfNecessary; + reparentItemsIfNecessary = [&reparentItemsIfNecessary,&reparentIfNecessary] (FlexBox & box, Component *newparent) { + for (auto & item : box.items ) { + if (item.associatedFlexBox) { + reparentItemsIfNecessary(*item.associatedFlexBox, newparent); + } + else if (item.associatedComponent) { + reparentIfNecessary(item.associatedComponent, newparent); + } + } + }; + + + if (totminh > getHeight()) { + // not enough vertical space, put the top items in the scrollable viewport + // may have to reparent them + reparentIfNecessary(m_stretchgroup.get(), m_groupcontainer.get()); + reparentItemsIfNecessary(togglesbox, m_groupcontainer.get()); + reparentItemsIfNecessary(volbox, m_groupcontainer.get()); + + groupsbox.items.insert(0, FlexItem(minw, stretchH, *m_stretchgroup).withMargin(groupmargin).withFlex(0)); + groupsbox.items.insert(0, FlexItem(minw, volh, volbox).withMargin(groupmargin).withFlex(0)); + groupsbox.items.insert(0, FlexItem(minw, toggleh, togglesbox).withMargin(groupmargin).withFlex(0)); + + useh += toggleh + volh + stretchH + 6*groupmargin; + + } else { + // may have to reparent them + reparentIfNecessary(m_stretchgroup.get(), this); + reparentItemsIfNecessary(togglesbox, this); + reparentItemsIfNecessary(volbox, this); + + mainbox.items.add(FlexItem(minw, toggleh, togglesbox).withMargin(margin).withFlex(0)); + mainbox.items.add(FlexItem(minw, volh, volbox).withMargin(margin).withFlex(0)); + mainbox.items.add(FlexItem(minw, stretchH, *m_stretchgroup).withMargin(margin).withFlex(0)); + } + + mainbox.items.add(FlexItem(6, 4)); + + + mainbox.items.add(FlexItem(w, vpminh, *m_groupviewport).withMargin(0).withFlex(1).withMaxHeight(useh + 4)); + + mainbox.items.add(FlexItem(6, 5)); + + mainbox.items.add(FlexItem(w, orderminh, m_spec_order_ed).withMargin(2).withFlex(0.1).withMaxHeight(60)); + + mainbox.items.add(FlexItem(6, 6)); + + mainbox.items.add(FlexItem(w, tabminh, m_wavefilter_tab).withMargin(0).withFlex(0.1)); + + mainbox.performLayout(bounds); + + + auto groupsbounds = Rectangle(0, 0, w-scrollw, useh); + m_groupcontainer->setBounds(groupsbounds); + groupsbox.performLayout(groupsbounds); + + + m_wavecomponent.setBounds(m_wave_container->getX(), 0, m_wave_container->getWidth(), m_wave_container->getHeight()-16); m_zs.setBounds(m_wave_container->getX(), m_wavecomponent.getBottom(), m_wave_container->getWidth(), 15); //m_wavecomponent.setBounds(1, m_spec_order_ed.getBottom()+1, getWidth()-2, remain_h/5*4); - - - + } void PaulstretchpluginAudioProcessorEditor::timerCallback(int id) @@ -491,7 +779,10 @@ void PaulstretchpluginAudioProcessorEditor::timerCallback(int id) if (processor.m_capture_save_state == 1) infotext += "Saving captured audio..."; m_info_label.setText(infotext, dontSendNotification); - + + for (auto & group : m_pargroups) { + group.second->updateParameterComponents(); + } } if (id == 2) { @@ -547,10 +838,15 @@ bool PaulstretchpluginAudioProcessorEditor::keyPressed(const KeyPress & press) void PaulstretchpluginAudioProcessorEditor::showSettingsMenu() { PopupMenu m_settings_menu; + if (JUCEApplicationBase::isStandaloneApp()) { + m_settings_menu.addItem(11, "Audio Setup...", true, false); + } m_settings_menu.addItem(4, "Reset parameters", true, false); + m_settings_menu.addSeparator(); m_settings_menu.addItem(5, "Load file with plugin state", true, processor.m_load_file_with_state); m_settings_menu.addItem(1, "Play when host transport running", true, processor.m_play_when_host_plays); m_settings_menu.addItem(2, "Capture when host transport running", true, processor.m_capture_when_host_plays); + m_settings_menu.addSeparator(); m_settings_menu.addItem(8, "Mute passthrough while capturing", true, processor.m_mute_while_capturing); m_settings_menu.addItem(10, "Mute processed audio output while capturing", true, processor.m_mute_processed_while_capturing); m_settings_menu.addItem(9, "Save captured audio to disk", true, processor.m_save_captured_audio); @@ -561,12 +857,21 @@ void PaulstretchpluginAudioProcessorEditor::showSettingsMenu() capturelenmenu.addItem(200+i, String(m_capturelens[i])+" seconds", true, capturelen == m_capturelens[i]); m_settings_menu.addSubMenu("Capture buffer length", capturelenmenu); + m_settings_menu.addSeparator(); m_settings_menu.addItem(3, "About...", true, false); #ifdef JUCE_DEBUG m_settings_menu.addItem(6, "Dump preset to clipboard", true, false); #endif m_settings_menu.addItem(7, "Show technical info", true, processor.m_show_technical_info); - m_settings_menu.showMenuAsync(PopupMenu::Options(), + + auto options = PopupMenu::Options().withTargetComponent(&m_settings_button); +#if JUCE_IOS + options = options.withStandardItemHeight(34); +#endif + if (!JUCEApplicationBase::isStandaloneApp()) { + options = options.withParentComponent(this); + } + m_settings_menu.showMenuAsync(options, ModalCallbackFunction::forComponent(handleSettingsMenuModalCallback, this)); } @@ -574,7 +879,7 @@ void PaulstretchpluginAudioProcessorEditor::showAbout() { String fftlib = fftwf_version; String juceversiontxt = String("JUCE ") + String(JUCE_MAJOR_VERSION) + "." + String(JUCE_MINOR_VERSION); - String title = g_plugintitle; + String title = String(JucePlugin_Name) + " " + String(JucePlugin_VersionString); #ifdef JUCE_DEBUG title += " (DEBUG)"; #endif @@ -583,18 +888,41 @@ void PaulstretchpluginAudioProcessorEditor::showAbout() processor.wrapperType == AudioProcessor::wrapperType_VST3) vstInfo = "VST Plug-In Technology by Steinberg.\n\n"; PluginHostType host; - AlertWindow::showMessageBoxAsync(AlertWindow::InfoIcon, - title, - "Plugin for extreme time stretching and other sound processing\nBuilt on " + String(__DATE__) + " " + String(__TIME__) + "\n" - "Copyright (C) 2006-2011 Nasca Octavian Paul, Tg. Mures, Romania\n" - "(C) 2017-2021 Xenakios\n\n"+vstInfo+ - "Using " + fftlib + " for FFT\n\n" - + juceversiontxt + " Used under the GPL license.\n\n" - "GPL licensed source code for this plugin at : https://bitbucket.org/xenakios/paulstretchplugin/overview\n" - "Running in : "+host.getHostDescription()+"\n" - , "OK", - this); + auto * content = new Label(); + String text = title + "\n\n" + + "Plugin for extreme time stretching and other sound processing\nBuilt on " + String(__DATE__) + " " + String(__TIME__) + "\n" + "Copyright (C) 2006-2011 Nasca Octavian Paul, Tg. Mures, Romania\n" + "(C) 2017-2021 Xenakios\n" + "(C) 2022 Jesse Chappell\n\n" + +vstInfo+ + "Using " + fftlib + " for FFT\n\n" + + juceversiontxt + " used under the GPL license.\n\n" + "GPL licensed source code for this plugin at : https://bitbucket.org/xenakios/paulstretchplugin/overview\n"; + + if (host.type != juce::PluginHostType::UnknownHost) { + text += "Running in : "+host.getHostDescription()+"\n"; + } + + content->setJustificationType(Justification::centred); + content->setText(text, dontSendNotification); + + auto wrap = std::make_unique(); + wrap->setViewedComponent(content, true); // takes ownership of content + + //std::unique_ptr contptr(content); + int defWidth = 450; + int defHeight = 350; +#if JUCE_IOS + defWidth = 320; + defHeight = 350; +#endif + + content->setSize (defWidth, defHeight); + wrap->setSize(jmin(defWidth, getWidth() - 20), jmin(defHeight, getHeight() - 24)); + + auto bounds = getLocalArea(nullptr, m_settings_button.getScreenBounds()); + CallOutBox::launchAsynchronously(std::move(wrap), bounds, this); } void PaulstretchpluginAudioProcessorEditor::toggleFileBrowser() @@ -714,10 +1042,13 @@ void WaveformComponent::paint(Graphics & g) } */ } + + Colour selcolor(0xffccaacc); + if (m_is_at_selection_drag_area) - g.setColour(Colours::white.withAlpha(0.6f)); + g.setColour(selcolor.withAlpha(0.45f)); else - g.setColour(Colours::white.withAlpha(0.5f)); + g.setColour(selcolor.withAlpha(0.4f)); double sel_len = m_time_sel_end - m_time_sel_start; //if (sel_len > 0.0 && sel_len < 1.0) @@ -1051,17 +1382,20 @@ void SpectralChainEditor::paint(Graphics & g) if (m_src == nullptr) return; - int box_w = getWidth() / m_order.size(); + int xoff = 3; + int yoff = 3; + int box_w = (getWidth() - 2*xoff) / m_order.size(); int box_h = getHeight(); + for (int i = 0; i < m_order.size(); ++i) { //if (i!=m_cur_index) - drawBox(g, i, i*box_w, 0, box_w - 20, box_h); + drawBox(g, i, i*box_w + xoff, yoff, box_w - 20, box_h - 2*yoff); if (i(i*box_w + (box_w - 20), box_h / 2, i*box_w + box_w, box_h / 2), 2.0f, 12.0f, 12.0f); + g.drawArrow(juce::Line(i*box_w + (box_w - 20) + xoff + 1, box_h / 2, i*box_w + box_w + xoff, box_h / 2), 2.0f, 12.0f, 12.0f); } if (m_drag_x>=0 && m_drag_x=0) - drawBox(g, m_cur_index, m_drag_x, 0, box_w - 30, box_h); + drawBox(g, m_cur_index, m_drag_x - m_downoffset_x + 5, yoff, box_w - 30, box_h - 2*yoff); } void SpectralChainEditor::setSource(StretchAudioSource * src) @@ -1074,36 +1408,45 @@ void SpectralChainEditor::setSource(StretchAudioSource * src) void SpectralChainEditor::mouseDown(const MouseEvent & ev) { m_did_drag = false; - int box_w = getWidth() / m_order.size(); + int xoff = 3; + int yoff = 3; + int box_w = (getWidth() - 2*xoff) / m_order.size(); int box_h = getHeight(); - m_cur_index = ev.x / box_w; + m_cur_index = (ev.x - xoff) / box_w; if (m_cur_index >= 0) { - if (ModuleSelectedCallback) - ModuleSelectedCallback(m_order[m_cur_index].m_index); - juce::Rectangle r(box_w*m_cur_index, 1, 12, 12); - if (r.contains(ev.x, ev.y)) + bool done = false; + juce::Rectangle r(box_w*m_cur_index + 3, 3, 15, 15); + if (r.contains(ev.x - xoff, ev.y - yoff)) { toggleBool(m_order[m_cur_index].m_enabled); repaint(); - return; + done = true; } - } - m_drag_x = ev.x; + + if (ModuleSelectedCallback) + ModuleSelectedCallback(m_order[m_cur_index].m_index); + + if (done) return; + } + m_drag_x = ev.x; + m_downoffset_x = ev.x - xoff - box_w*m_cur_index; repaint(); } void SpectralChainEditor::mouseDrag(const MouseEvent & ev) { - int box_w = getWidth() / m_order.size(); - juce::Rectangle r(box_w*m_cur_index, 1, 12, 12); - if (r.contains(ev.x, ev.y)) + int xoff = 3; + int yoff = 3; + int box_w = (getWidth() - 2*xoff) / m_order.size(); + juce::Rectangle r(box_w*m_cur_index + 3, 3, 15, 15); + if (r.contains(ev.x - xoff, ev.y - yoff)) return; if (m_cur_index >= 0 && m_cur_index < m_order.size()) { int box_h = getHeight(); - int new_index = ev.x / box_w; + int new_index = (ev.x - xoff) / box_w; if (new_index >= 0 && new_index < m_order.size() && new_index != m_cur_index) { swapSpectrumProcesses(m_order[m_cur_index], m_order[new_index]); @@ -1172,20 +1515,26 @@ void SpectralChainEditor::drawBox(Graphics & g, int index, int x, int y, int w, txt = "Free filter"; if (index == m_cur_index) { - g.setColour(Colours::darkgrey); + g.setColour(Colour(0xff222222)); //g.fillRect(i*box_w, 0, box_w - 30, box_h - 1); - g.fillRect(x, y, w, h); + //g.fillRect(x, y, w, h); + g.fillRoundedRectangle(x, y, w, h, 4.0f); } - g.setColour(Colours::white); - g.drawRect(x, y, w, h); - g.drawFittedText(txt, x,y,w,h-5, Justification::centredBottom, 3); - //g.drawFittedText(m_order[index].m_enabled->name, x, y, w, h, Justification::centred, 3); + g.setColour(Colour(0xccaaaaaa)); + //g.drawRect(x, y, w, h); + g.drawRoundedRectangle(x, y, w, h, 4.0f, 1.0f); + + g.setColour(Colour(0xffaaaaaa)); + if (w > 10) { + g.drawFittedText(txt, x + 2,y,w-4,h-4, Justification::centredBottom, 3); + //g.drawFittedText(m_order[index].m_enabled->name, x, y, w, h, Justification::centred, 3); + } g.setColour(Colours::gold); - g.drawRect(x + 2, y + 2, 12, 12); + g.drawRect(x + 3, y + 3, 12, 12); if ((bool)*m_order[index].m_enabled == true) { - g.drawLine(x+2, y+2, x+14, y+14); - g.drawLine(x+2, y+14, x+14, y+2); + g.drawLine(x+3, y+3, x+15, y+15); + g.drawLine(x+3, y+15, x+15, y+3); } g.setColour(Colours::white); } @@ -1195,6 +1544,8 @@ ParameterComponent::ParameterComponent(AudioProcessorParameter * par, bool notif addAndMakeVisible(&m_label); m_labeldefcolor = m_label.findColour(Label::textColourId); m_label.setText(par->getName(50), dontSendNotification); + m_label.setJustificationType(Justification::centredRight); + AudioParameterFloat* floatpar = dynamic_cast(par); if (floatpar) { @@ -1202,8 +1553,10 @@ ParameterComponent::ParameterComponent(AudioProcessorParameter * par, bool notif m_notify_only_on_release = notifyOnlyOnRelease; m_slider->setRange(floatpar->range.start, floatpar->range.end, floatpar->range.interval); m_slider->setValue(*floatpar, dontSendNotification); + m_slider->setTextBoxStyle(Slider::TextBoxLeft, false, 60, 34); m_slider->addListener(this); m_slider->setDoubleClickReturnValue(true, floatpar->range.convertFrom0to1(par->getDefaultValue())); + m_slider->setViewportIgnoreDragFlag(true); } AudioParameterInt* intpar = dynamic_cast(par); if (intpar) @@ -1212,7 +1565,9 @@ ParameterComponent::ParameterComponent(AudioProcessorParameter * par, bool notif m_notify_only_on_release = notifyOnlyOnRelease; m_slider->setRange(intpar->getRange().getStart(), intpar->getRange().getEnd(), 1.0); m_slider->setValue(*intpar, dontSendNotification); + m_slider->setTextBoxStyle(Slider::TextBoxLeft, false, 60, 34); m_slider->addListener(this); + m_slider->setViewportIgnoreDragFlag(true); } AudioParameterChoice* choicepar = dynamic_cast(par); if (choicepar) @@ -1232,16 +1587,18 @@ ParameterComponent::ParameterComponent(AudioProcessorParameter * par, bool notif void ParameterComponent::resized() { + int h = getHeight(); if (m_slider) { - int labw = 200; - if (getWidth() < 400) - labw = 100; - m_label.setBounds(0, 0, labw, 24); - m_slider->setBounds(m_label.getRight() + 1, 0, getWidth() - 2 - m_label.getWidth(), 24); + //int labw = 200; + int labw = 120; + if (getWidth() < 350) + labw = 100; + m_label.setBounds(0, 0, labw, h); + m_slider->setBounds(m_label.getRight() + 1, 0, getWidth() - 2 - m_label.getWidth(), h); } if (m_togglebut) - m_togglebut->setBounds(1, 0, getWidth() - 1, 24); + m_togglebut->setBounds(1, 0, getWidth() - 1, h); } void ParameterComponent::sliderValueChanged(Slider * slid) @@ -1354,12 +1711,13 @@ void PerfMeterComponent::paint(Graphics & g) m_gradient.point2 = {(float)getWidth(),0.0f}; g.fillAll(Colours::grey); double amt = m_proc->getPreBufferingPercent(); - g.setColour(Colours::green); + g.setColour(Colours::green.withAlpha(0.8f)); int w = amt * getWidth(); //g.setGradientFill(m_gradient); g.fillRect(0, 0, w, getHeight()); - g.setColour(Colours::white); + g.setColour(Colours::white.withAlpha(0.4f)); g.drawRect(0, 0, getWidth(), getHeight()); + g.setColour(Colours::white); g.setFont(10.0f); if (m_proc->getPreBufferAmount()>0) g.drawText("PREBUFFER", 0, 0, getWidth(), getHeight(), Justification::centred); @@ -1378,7 +1736,15 @@ void PerfMeterComponent::mouseDown(const MouseEvent & ev) bufferingmenu.addItem(104, "Very large", true, curbufamount == 4); bufferingmenu.addItem(105, "Huge", true, curbufamount == 5); - auto opts = PopupMenu::Options(); + auto opts = PopupMenu::Options().withTargetComponent(this); + if (!JUCEApplicationBase::isStandaloneApp()) { + if (auto * editor = findParentComponentOfClass()) { + opts = opts.withParentComponent(editor); + } + } +#if JUCE_IOS + opts = opts.withStandardItemHeight(34); +#endif bufferingmenu.showMenuAsync(opts, [this](int r) { if (r >= 100 && r < 200) @@ -1524,7 +1890,7 @@ void RatioMixerEditor::resized() for (int i = 0; i < nsliders; ++i) { m_ratio_level_sliders[i]->setBounds(slidw/2+slidw * i-10, 15, 20, getHeight() - 55); - m_ratio_sliders[i]->setBounds(slidw * i, getHeight() - 48, slidw - 5, 47); + m_ratio_sliders[i]->setBounds(slidw * i, getHeight() - 48, slidw - 5, 45); } } @@ -1545,7 +1911,7 @@ void RatioMixerEditor::timerCallback() void RatioMixerEditor::paint(Graphics & g) { - g.fillAll(Colours::grey); + g.fillAll(Colour(0xff222222)); g.setColour(Colours::white); auto nsliders = m_ratio_sliders.size(); int slidw = getWidth() / nsliders; @@ -1576,17 +1942,38 @@ FreeFilterComponent::FreeFilterComponent(PaulstretchpluginAudioProcessor* proc) void FreeFilterComponent::resized() { + int minslidwidth = 230; + int slidh = 24; + int margin = 1; + +#if JUCE_IOS + slidh = 28; +#endif + + FlexBox mainbox; + mainbox.flexDirection = FlexBox::Direction::row; + + m_env.setBounds(m_slidwidth, 0, getWidth() - m_slidwidth, getHeight()); - for (int i = 0; i < m_parcomps.size(); ++i) + + FlexBox slidbox; + slidbox.flexDirection = FlexBox::Direction::column; + + for (int i = 0; i < m_parcomps.size(); ++i) { - m_parcomps[i]->setBounds(1, 1+25*i, m_slidwidth - 2, 24); - } - + //m_parcomps[i]->setBounds(1, 1+25*i, m_slidwidth - 2, 24); + slidbox.items.add(FlexItem(minslidwidth, slidh, *m_parcomps[i]).withMargin(margin).withFlex(0)); + } + + mainbox.items.add(FlexItem(minslidwidth, 50, slidbox).withMargin(0).withFlex(1).withMaxWidth(m_slidwidth)); + mainbox.items.add(FlexItem(100, 50, m_env).withMargin(0).withFlex(3)); + mainbox.performLayout(getLocalBounds()); } void FreeFilterComponent::paint(Graphics & g) { - g.setColour(Colours::grey); + g.setColour(Colour(0xff222222)); + g.fillRect(0, 0, m_slidwidth, getHeight()); } @@ -1596,6 +1983,148 @@ void FreeFilterComponent::updateParameterComponents() e->updateComponent(); } +ParameterGroupComponent::ParameterGroupComponent(const String & name_, int groupid, PaulstretchpluginAudioProcessor* proc, bool showtoggle) +:name(name_), groupId(groupid), m_proc(proc), m_bgcolor(0xff1a1a1a) +{ + if (name_.isNotEmpty()) { + m_namelabel = std::make_unique