Tchick/Source/OptionsPanel.cpp
2026-02-04 11:51:22 +01:00

319 lines
10 KiB
C++

/*
==============================================================================
Copyright 2022 Nicolas Chambert
This program is free software : you can redistribute itand /or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program.If not, see < http://www.gnu.org/licenses/>.
==============================================================================
*/
/*
==============================================================================
Gestion des options figure rythmique et fichier son du tick.
==============================================================================
*/
#include "OptionsPanel.h"
#include "Figures.h"
void ClickSounds::init()
{
// on récupère tout les sous dossiers dans App/Clicks
juce::File curDir{ juce::File::getSpecialLocation(juce::File::SpecialLocationType::currentExecutableFile).getParentDirectory() };
juce::File clickDir{ curDir.getChildFile("Clicks")};
auto it = clickDir.findChildFiles(juce::File::findDirectories, false);
for (int i = 0; i < it.size(); ++i)
{
juce::File sub = it[i];
bool highFile = !sub.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false, "High*.wav").isEmpty();
bool lowFile = !sub.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false, "Low*.wav").isEmpty();
if (highFile && lowFile)
{
// On n'ajoute que si on a bien les fichiers High et Low
juce::String path = sub.getFullPathName();
std::string ss = path.toStdString();
sounds.push_back(ss);
}
}
}
std::string ClickSounds::getSoundName(int i) const
{
// Valeur affiché dans la combo, on récupère le nom du sous dossier
juce::File path{ sounds[i] };
return path.getFileNameWithoutExtension().toStdString();
}
std::string ClickSounds::getSoundPathHigh(int i) const
{
// Chemin vers le fichier High
juce::File path{ sounds[i] };
return path.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false, "High*.wav")[0].getFullPathName().toStdString();
}
std::string ClickSounds::getSoundPathLow(int i) const
{
// Chemin vers le fichier Low
juce::File path{ sounds[i] };
return path.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false, "Low*.wav")[0].getFullPathName().toStdString();
}
int ClickSounds::getIndex(std::string soundName) const
{
// récupère l'index du fichier à partir de son nom
// Utilisé pour restaurer la configuration
// Du coup si le son n'est pas trouvé on retourne le premier son
for (int i = 0; i < sounds.size(); ++i)
{
if (getSoundName(i) == soundName)
{
return i;
}
}
return 0;
}
void ComboLookAndFeel::drawPopupMenuBackground(juce::Graphics& g, int width, int height)
{
// Permet de setter le background du PopupMenu
g.fillAll(juce::Colours::whitesmoke);
juce::ignoreUnused(width, height);
#if ! JUCE_MAC
g.setColour(juce::Colours::darkgrey.withAlpha(0.6f));
g.drawRect(0, 0, width, height);
#endif
}
void ComboLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle<int>& area,
const bool isSeparator, const bool isActive,
const bool isHighlighted, const bool isTicked,
const bool hasSubMenu, const juce::String& text,
const juce::String& shortcutKeyText,
const juce::Drawable* icon, const juce::Colour* const textColourToUse)
{
// Permet de setter le background du PopupMenuItem
if (isSeparator)
{
auto r = area.reduced(5, 0);
r.removeFromTop(juce::roundToInt(((float)r.getHeight() * 0.5f) - 0.5f));
g.setColour(juce::Colours::darkgrey.withAlpha(0.3f));
g.fillRect(r.removeFromTop(1));
}
else
{
auto textColour = (textColourToUse == nullptr ? juce::Colours::darkgrey
: *textColourToUse);
auto r = area.reduced(1);
if (isHighlighted && isActive)
{
g.setColour(juce::Colours::lightgrey);
g.fillRect(r);
g.setColour(juce::Colours::darkgrey);
}
else
{
g.setColour(textColour.withMultipliedAlpha(isActive ? 1.0f : 0.5f));
}
r.reduce(juce::jmin(5, area.getWidth() / 20), 0);
auto font = getPopupMenuFont();
auto maxFontHeight = (float)r.getHeight() / 1.3f;
if (font.getHeight() > maxFontHeight)
font.setHeight(maxFontHeight);
g.setFont(font);
auto iconArea = r.removeFromLeft(juce::roundToInt(maxFontHeight)).toFloat();
if (icon != nullptr)
{
icon->drawWithin(g, iconArea, juce::RectanglePlacement::centred | juce::RectanglePlacement::onlyReduceInSize, 1.0f);
r.removeFromLeft(juce::roundToInt(maxFontHeight * 0.5f));
}
else if (isTicked)
{
auto tick = getTickShape(1.0f);
g.fillPath(tick, tick.getTransformToScaleToFit(iconArea.reduced(iconArea.getWidth() / 5, 0).toFloat(), true));
}
if (hasSubMenu)
{
auto arrowH = 0.6f * getPopupMenuFont().getAscent();
auto x = static_cast<float> (r.removeFromRight((int)arrowH).getX());
auto halfH = static_cast<float> (r.getCentreY());
juce::Path path;
path.startNewSubPath(x, halfH - arrowH * 0.5f);
path.lineTo(x + arrowH * 0.6f, halfH);
path.lineTo(x, halfH + arrowH * 0.5f);
g.strokePath(path, juce::PathStrokeType(2.0f));
}
r.removeFromRight(3);
g.drawFittedText(text, r, juce::Justification::centredLeft, 1);
if (shortcutKeyText.isNotEmpty())
{
auto f2 = font;
f2.setHeight(f2.getHeight() * 0.75f);
f2.setHorizontalScale(0.95f);
g.setFont(f2);
g.drawText(shortcutKeyText, r, juce::Justification::centredRight, true);
}
}
}
OptionsPanel::OptionsPanel(juce::Colour c, Config& configuration) : backgroundColour(c)
{
// initialisation de l'interface
clef.reset(new ComboLookAndFeel());
addAndMakeVisible(figures);
auto doc = juce::XmlDocument::parse(SVGFigures::noire); // on charge la noire par defaut, va être re-initialisé par la configuration
auto note = juce::Drawable::createFromSVG(*doc);
figures.setImages(note.get());
figures.setToggleState(false, juce::NotificationType::dontSendNotification);
figures.setColour(juce::TextButton::buttonColourId, juce::Colours::whitesmoke);
figures.setColour(juce::TextButton::textColourOffId, juce::Colours::black);
figures.onClick = [&]
{
// génère le dropdown menu des figures
juce::PopupMenu menu;
int width = 70;
int height = 48;
menu.addCustomItem(1, std::make_unique<FigureButton>(SVGFigures::blanche, width, height, juce::Colours::whitesmoke));
menu.addCustomItem(4, std::make_unique<FigureButton>(SVGFigures::triolet, width, height, juce::Colours::whitesmoke));
menu.addCustomItem(2, std::make_unique<FigureButton>(SVGFigures::noire, width, height, juce::Colours::whitesmoke));
menu.addCustomItem(5, std::make_unique<FigureButton>(SVGFigures::doubles, width, height, juce::Colours::whitesmoke));
menu.addCustomItem(3, std::make_unique<FigureButton>(SVGFigures::croches, width, height, juce::Colours::whitesmoke));
menu.addCustomItem(6, std::make_unique<FigureButton>(SVGFigures::swing, width, height, juce::Colours::whitesmoke));
menu.showMenuAsync(juce::PopupMenu::Options{}.withTargetComponent(figures)
.withMinimumNumColumns(3), [&](int resultId) { if(resultId > 0) setFigure(resultId - 1); });
};
addAndMakeVisible(sounds);
sounds.setLookAndFeel(clef.get());
sounds.setColour(juce::ComboBox::backgroundColourId, juce::Colours::whitesmoke);
sounds.setColour(juce::ComboBox::outlineColourId, juce::Colours::darkgrey);
sounds.setColour(juce::ComboBox::arrowColourId, juce::Colours::darkgrey);
sounds.setColour(juce::ComboBox::textColourId, juce::Colours::darkgrey);
clicksounds.init(); // charge la liste des sons
curSoundID = clicksounds.getIndex(configuration.sound.toStdString()); // récupère le son courant dans la configuration
}
juce::String OptionsPanel::getSoundName()
{
// Récupère le nom du son courant pour sauver la config
return clicksounds.getSoundName(sounds.getSelectedId() - 1);
}
void OptionsPanel::setFigure(int figure)
{
// MàJ de l'icone de la figure courante
juce::String newSVG;
switch (figure)
{
case 0:
newSVG = SVGFigures::blanche;
break;
case 2:
newSVG = SVGFigures::croches;
break;
case 3:
newSVG = SVGFigures::triolet;
break;
case 4:
newSVG = SVGFigures::doubles;
break;
case 5:
newSVG = SVGFigures::swing;
break;
case 1:
default:
newSVG = SVGFigures::noire;
break;
}
auto doc = juce::XmlDocument::parse(newSVG);
auto note = juce::Drawable::createFromSVG(*doc);
figures.setImages(note.get());
setMetroFigure(figure);
}
OptionsPanel::~OptionsPanel()
{
// evite une fuite mémoire à la fermeture due au L&F
sounds.setLookAndFeel(nullptr);
figures.setLookAndFeel(nullptr);
}
void OptionsPanel::paint(juce::Graphics& g)
{
g.fillAll(backgroundColour);
}
void OptionsPanel::resized()
{
// On utilise un layout grille en 2 colonnes
int margin = 3;
juce::Grid grid;
using Track = juce::Grid::TrackInfo;
using Fr = juce::Grid::Fr;
grid.templateRows = { Track(Fr(1)) };
grid.templateColumns = { Track(Fr(1)), Track(Fr(1)) };
grid.items = {
juce::GridItem(figures).withMargin(juce::GridItem::Margin(margin)),
juce::GridItem(sounds).withMargin(juce::GridItem::Margin(margin)),
};
grid.performLayout(getLocalBounds());
}
void OptionsPanel::init(Metronome& metro)
{
// On établi les liaisons entre le Metronome et les composants graphique
for (int i = 0; i < clicksounds.getSoundCount(); ++i)
{
sounds.addItem(clicksounds.getSoundName(i), i + 1); // La combo n'autorise pas 0 comme Id, d'ou le + 1
}
sounds.setSelectedId(curSoundID + 1); // toujours ce + 1
selectSound(metro); // charge le son courant dans le metronome
sounds.onChange = [&, this]() { selectSound(metro); };
setMetroFigure = [&](int figure) { metro.setFigure(figure); };
setFigure((int)metro.getFigure());
}
void OptionsPanel::selectSound(Metronome& metro)
{
// charge le son courant dans le metronome
std::string high = clicksounds.getSoundPathHigh(sounds.getSelectedId() - 1);
std::string low = clicksounds.getSoundPathLow(sounds.getSelectedId() - 1);
metro.loadSounds(high, low);
}