2.3 update
Preset management. Dark background menu. JUCE 6.0.8 update.
This commit is contained in:
parent
92383753b0
commit
576e6f52bc
7 changed files with 735 additions and 50 deletions
206
Source/Components/SetPresetNameWindow.cpp
Normal file
206
Source/Components/SetPresetNameWindow.cpp
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This is an automatically generated GUI class created by the Projucer!
|
||||||
|
|
||||||
|
Be careful when adding custom code to these files, as only the code within
|
||||||
|
the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
|
||||||
|
and re-saved.
|
||||||
|
|
||||||
|
Created with Projucer version: 6.0.8
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
The Projucer is part of the JUCE library.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
//[Headers] You can add your own extra header files here...
|
||||||
|
//[/Headers]
|
||||||
|
|
||||||
|
#include "SetPresetNameWindow.h"
|
||||||
|
|
||||||
|
|
||||||
|
//[MiscUserDefs] You can add your own user definitions and misc code here...
|
||||||
|
//[/MiscUserDefs]
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
SetPresetNameWindow::SetPresetNameWindow ()
|
||||||
|
{
|
||||||
|
//[Constructor_pre] You can add your own custom stuff here..
|
||||||
|
//[/Constructor_pre]
|
||||||
|
|
||||||
|
nameTextEditor.reset (new juce::TextEditor ("nameTextEditor"));
|
||||||
|
addAndMakeVisible (nameTextEditor.get());
|
||||||
|
nameTextEditor->setMultiLine (false);
|
||||||
|
nameTextEditor->setReturnKeyStartsNewLine (false);
|
||||||
|
nameTextEditor->setReadOnly (false);
|
||||||
|
nameTextEditor->setScrollbarsShown (true);
|
||||||
|
nameTextEditor->setCaretVisible (true);
|
||||||
|
nameTextEditor->setPopupMenuEnabled (false);
|
||||||
|
nameTextEditor->setColour (juce::TextEditor::backgroundColourId, juce::Colours::black);
|
||||||
|
nameTextEditor->setColour (juce::CaretComponent::caretColourId, juce::Colours::white);
|
||||||
|
nameTextEditor->setText (juce::String());
|
||||||
|
|
||||||
|
cancel.reset (new juce::TextButton ("cancel"));
|
||||||
|
addAndMakeVisible (cancel.get());
|
||||||
|
cancel->setButtonText (TRANS("Cancel"));
|
||||||
|
cancel->addListener (this);
|
||||||
|
cancel->setColour (juce::TextButton::buttonColourId, juce::Colours::black);
|
||||||
|
|
||||||
|
Ok.reset (new juce::TextButton ("Ok"));
|
||||||
|
addAndMakeVisible (Ok.get());
|
||||||
|
Ok->setButtonText (TRANS("OK"));
|
||||||
|
Ok->addListener (this);
|
||||||
|
Ok->setColour (juce::TextButton::buttonColourId, juce::Colours::black);
|
||||||
|
|
||||||
|
|
||||||
|
//[UserPreSize]
|
||||||
|
cancel->setColour (juce::ComboBox::ColourIds::outlineColourId, juce::Colours::white);
|
||||||
|
Ok->setColour (juce::ComboBox::ColourIds::outlineColourId, juce::Colours::white);
|
||||||
|
//[/UserPreSize]
|
||||||
|
|
||||||
|
setSize (300, 150);
|
||||||
|
|
||||||
|
|
||||||
|
//[Constructor] You can add your own custom stuff here..
|
||||||
|
//[/Constructor]
|
||||||
|
}
|
||||||
|
|
||||||
|
SetPresetNameWindow::~SetPresetNameWindow()
|
||||||
|
{
|
||||||
|
//[Destructor_pre]. You can add your own custom destruction code here..
|
||||||
|
//[/Destructor_pre]
|
||||||
|
|
||||||
|
nameTextEditor = nullptr;
|
||||||
|
cancel = nullptr;
|
||||||
|
Ok = nullptr;
|
||||||
|
|
||||||
|
|
||||||
|
//[Destructor]. You can add your own custom destruction code here..
|
||||||
|
//[/Destructor]
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void SetPresetNameWindow::paint (juce::Graphics& g)
|
||||||
|
{
|
||||||
|
//[UserPrePaint] Add your own custom painting code here..
|
||||||
|
//[/UserPrePaint]
|
||||||
|
|
||||||
|
g.fillAll (juce::Colours::black);
|
||||||
|
|
||||||
|
{
|
||||||
|
int x = 0, y = proportionOfHeight (0.0000f), width = proportionOfWidth (1.0000f), height = proportionOfHeight (1.0000f);
|
||||||
|
juce::Colour fillColour = juce::Colours::black;
|
||||||
|
juce::Colour strokeColour = juce::Colour (0xff666666);
|
||||||
|
//[UserPaintCustomArguments] Customize the painting arguments here..
|
||||||
|
//[/UserPaintCustomArguments]
|
||||||
|
g.setColour (fillColour);
|
||||||
|
g.fillRect (x, y, width, height);
|
||||||
|
g.setColour (strokeColour);
|
||||||
|
g.drawRect (x, y, width, height, 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
int x = proportionOfWidth (0.0000f), y = proportionOfHeight (0.1000f), width = proportionOfWidth (1.0000f), height = proportionOfHeight (0.2000f);
|
||||||
|
juce::String text (TRANS("Preset Name"));
|
||||||
|
juce::Colour fillColour = juce::Colours::white;
|
||||||
|
//[UserPaintCustomArguments] Customize the painting arguments here..
|
||||||
|
//[/UserPaintCustomArguments]
|
||||||
|
g.setColour (fillColour);
|
||||||
|
g.setFont (juce::Font (15.00f, juce::Font::plain).withTypefaceStyle ("Regular"));
|
||||||
|
g.drawText (text, x, y, width, height,
|
||||||
|
juce::Justification::centred, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//[UserPaint] Add your own custom painting code here..
|
||||||
|
//[/UserPaint]
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPresetNameWindow::resized()
|
||||||
|
{
|
||||||
|
//[UserPreResize] Add your own custom resize code here..
|
||||||
|
//[/UserPreResize]
|
||||||
|
|
||||||
|
nameTextEditor->setBounds (proportionOfWidth (0.1500f), proportionOfHeight (0.3467f), proportionOfWidth (0.7000f), proportionOfHeight (0.1733f));
|
||||||
|
cancel->setBounds (proportionOfWidth (0.2000f), proportionOfHeight (0.7000f), proportionOfWidth (0.2500f), proportionOfHeight (0.1600f));
|
||||||
|
Ok->setBounds (proportionOfWidth (0.5500f), proportionOfHeight (0.7000f), proportionOfWidth (0.2500f), proportionOfHeight (0.1600f));
|
||||||
|
//[UserResized] Add your own custom resize handling here..
|
||||||
|
//[/UserResized]
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPresetNameWindow::buttonClicked (juce::Button* buttonThatWasClicked)
|
||||||
|
{
|
||||||
|
//[UserbuttonClicked_Pre]
|
||||||
|
//[/UserbuttonClicked_Pre]
|
||||||
|
|
||||||
|
if (buttonThatWasClicked == cancel.get())
|
||||||
|
{
|
||||||
|
//[UserButtonCode_cancel] -- add your button handler code here..
|
||||||
|
callback(0, nameTextEditor->getText());
|
||||||
|
//[/UserButtonCode_cancel]
|
||||||
|
}
|
||||||
|
else if (buttonThatWasClicked == Ok.get())
|
||||||
|
{
|
||||||
|
//[UserButtonCode_Ok] -- add your button handler code here..
|
||||||
|
callback(1, nameTextEditor->getText());
|
||||||
|
//[/UserButtonCode_Ok]
|
||||||
|
}
|
||||||
|
|
||||||
|
//[UserbuttonClicked_Post]
|
||||||
|
//[/UserbuttonClicked_Post]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
|
||||||
|
void SetPresetNameWindow::grabTextEditorFocus()
|
||||||
|
{
|
||||||
|
nameTextEditor->grabKeyboardFocus();
|
||||||
|
};
|
||||||
|
//[/MiscUserCode]
|
||||||
|
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
#if 0
|
||||||
|
/* -- Projucer information section --
|
||||||
|
|
||||||
|
This is where the Projucer stores the metadata that describe this GUI layout, so
|
||||||
|
make changes in here at your peril!
|
||||||
|
|
||||||
|
BEGIN_JUCER_METADATA
|
||||||
|
|
||||||
|
<JUCER_COMPONENT documentType="Component" className="SetPresetNameWindow" componentName=""
|
||||||
|
parentClasses="public juce::Component" constructorParams="" variableInitialisers=""
|
||||||
|
snapPixels="8" snapActive="1" snapShown="1" overlayOpacity="0.330"
|
||||||
|
fixedSize="1" initialWidth="300" initialHeight="150">
|
||||||
|
<BACKGROUND backgroundColour="ff000000">
|
||||||
|
<RECT pos="0 0% 100% 100%" fill="solid: ff000000" hasStroke="1" stroke="1, mitered, butt"
|
||||||
|
strokeColour="solid: ff666666"/>
|
||||||
|
<TEXT pos="0% 10% 100% 20%" fill="solid: ffffffff" hasStroke="0" text="Preset Name"
|
||||||
|
fontname="Default font" fontsize="15.0" kerning="0.0" bold="0"
|
||||||
|
italic="0" justification="36"/>
|
||||||
|
</BACKGROUND>
|
||||||
|
<TEXTEDITOR name="nameTextEditor" id="13e287a1045d7d6d" memberName="nameTextEditor"
|
||||||
|
virtualName="" explicitFocusOrder="0" pos="15% 34.667% 70% 17.333%"
|
||||||
|
bkgcol="ff000000" caretcol="ffffffff" initialText="" multiline="0"
|
||||||
|
retKeyStartsLine="0" readonly="0" scrollbars="1" caret="1" popupmenu="0"/>
|
||||||
|
<TEXTBUTTON name="cancel" id="873979f2630a3992" memberName="cancel" virtualName=""
|
||||||
|
explicitFocusOrder="0" pos="20% 70% 25% 16%" bgColOff="ff000000"
|
||||||
|
buttonText="Cancel" connectedEdges="0" needsCallback="1" radioGroupId="0"/>
|
||||||
|
<TEXTBUTTON name="Ok" id="2874357d53dac91e" memberName="Ok" virtualName=""
|
||||||
|
explicitFocusOrder="0" pos="55% 70% 25% 16%" bgColOff="ff000000"
|
||||||
|
buttonText="OK" connectedEdges="0" needsCallback="1" radioGroupId="0"/>
|
||||||
|
</JUCER_COMPONENT>
|
||||||
|
|
||||||
|
END_JUCER_METADATA
|
||||||
|
*/
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
//[EndFile] You can add extra defines here...
|
||||||
|
//[/EndFile]
|
||||||
|
|
75
Source/Components/SetPresetNameWindow.h
Normal file
75
Source/Components/SetPresetNameWindow.h
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This is an automatically generated GUI class created by the Projucer!
|
||||||
|
|
||||||
|
Be careful when adding custom code to these files, as only the code within
|
||||||
|
the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
|
||||||
|
and re-saved.
|
||||||
|
|
||||||
|
Created with Projucer version: 6.0.8
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
The Projucer is part of the JUCE library.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//[Headers] -- You can add your own extra header files here --
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
//[/Headers]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
/**
|
||||||
|
//[Comments]
|
||||||
|
An auto-generated component, created by the Projucer.
|
||||||
|
|
||||||
|
Describe your class and how it works here!
|
||||||
|
//[/Comments]
|
||||||
|
*/
|
||||||
|
class SetPresetNameWindow : public juce::Component,
|
||||||
|
public juce::Button::Listener
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
SetPresetNameWindow ();
|
||||||
|
~SetPresetNameWindow() override;
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
//[UserMethods] -- You can add your own custom methods in this section.
|
||||||
|
std::function<void(int, juce::String)> callback;
|
||||||
|
void setText(const String &txt){
|
||||||
|
nameTextEditor->setText(txt);
|
||||||
|
}
|
||||||
|
void grabTextEditorFocus();
|
||||||
|
//[/UserMethods]
|
||||||
|
|
||||||
|
void paint (juce::Graphics& g) override;
|
||||||
|
void resized() override;
|
||||||
|
void buttonClicked (juce::Button* buttonThatWasClicked) override;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
//[UserVariables] -- You can add your own custom variables in this section.
|
||||||
|
//[/UserVariables]
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
std::unique_ptr<juce::TextEditor> nameTextEditor;
|
||||||
|
std::unique_ptr<juce::TextButton> cancel;
|
||||||
|
std::unique_ptr<juce::TextButton> Ok;
|
||||||
|
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SetPresetNameWindow)
|
||||||
|
};
|
||||||
|
|
||||||
|
//[EndFile] You can add extra defines here...
|
||||||
|
//[/EndFile]
|
||||||
|
|
BIN
Source/Images/main.psd
Normal file
BIN
Source/Images/main.psd
Normal file
Binary file not shown.
|
@ -16,14 +16,19 @@ It contains the basic startup code for a Juce application.
|
||||||
ObxdAudioProcessorEditor::ObxdAudioProcessorEditor (ObxdAudioProcessor& ownerFilter)
|
ObxdAudioProcessorEditor::ObxdAudioProcessorEditor (ObxdAudioProcessor& ownerFilter)
|
||||||
: AudioProcessorEditor (&ownerFilter), processor (ownerFilter),
|
: AudioProcessorEditor (&ownerFilter), processor (ownerFilter),
|
||||||
skinFolder (processor.getSkinFolder()),
|
skinFolder (processor.getSkinFolder()),
|
||||||
progStart (2000),
|
progStart (3000),
|
||||||
bankStart (1000),
|
bankStart (2000),
|
||||||
skinStart (0),
|
skinStart (1000),
|
||||||
skins (processor.getSkinFiles()),
|
skins (processor.getSkinFiles()),
|
||||||
banks (processor.getBankFiles())
|
banks (processor.getBankFiles())
|
||||||
{
|
{
|
||||||
|
LookAndFeel& lf = getLookAndFeel();
|
||||||
|
// Popup Menu Look and Feel
|
||||||
|
lf.setColour(PopupMenu::backgroundColourId, Colour(20, 20, 20));
|
||||||
|
lf.setColour(PopupMenu::textColourId, Colour(245, 245, 245));
|
||||||
|
lf.setColour(PopupMenu::highlightedBackgroundColourId, Colour(60, 60, 60));
|
||||||
|
|
||||||
// skinFolder = ownerFilter.getCurrentSkinFolder(); // initialized above
|
//skinFolder = ownerFilter.getCurrentSkinFolder(); // initialized above
|
||||||
commandManager.registerAllCommandsForTarget(this);
|
commandManager.registerAllCommandsForTarget(this);
|
||||||
commandManager.setFirstCommandTarget(this);
|
commandManager.setFirstCommandTarget(this);
|
||||||
|
|
||||||
|
@ -51,6 +56,26 @@ ObxdAudioProcessorEditor::ObxdAudioProcessorEditor (ObxdAudioProcessor& ownerFil
|
||||||
updateFromHost();
|
updateFromHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ObxdAudioProcessorEditor::resized() {
|
||||||
|
if (setPresetNameWindow != nullptr )
|
||||||
|
{
|
||||||
|
if (auto wrapper = dynamic_cast<ObxdAudioProcessorEditor*>(processor.getActiveEditor()))
|
||||||
|
{
|
||||||
|
|
||||||
|
auto w = proportionOfWidth(0.25f);
|
||||||
|
auto h = proportionOfHeight(0.3f);
|
||||||
|
auto x = proportionOfWidth(0.5f) - (w / 2);
|
||||||
|
auto y = wrapper->getY();
|
||||||
|
|
||||||
|
if (setPresetNameWindow != nullptr)
|
||||||
|
{
|
||||||
|
y += proportionOfHeight(0.15f);
|
||||||
|
setPresetNameWindow->setBounds(x, y, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
|
void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
|
||||||
{
|
{
|
||||||
knobAttachments.clear();
|
knobAttachments.clear();
|
||||||
|
@ -377,13 +402,64 @@ void ObxdAudioProcessorEditor::rebuildComponents (ObxdAudioProcessor& ownerFilte
|
||||||
|
|
||||||
void ObxdAudioProcessorEditor::createMenu ()
|
void ObxdAudioProcessorEditor::createMenu ()
|
||||||
{
|
{
|
||||||
|
popupMenus.clear();
|
||||||
PopupMenu* menu = new PopupMenu();
|
PopupMenu* menu = new PopupMenu();
|
||||||
PopupMenu progMenu;
|
PopupMenu progMenu;
|
||||||
PopupMenu bankMenu;
|
PopupMenu bankMenu;
|
||||||
PopupMenu skinMenu;
|
PopupMenu skinMenu;
|
||||||
|
PopupMenu fileMenu;
|
||||||
skins = processor.getSkinFiles();
|
skins = processor.getSkinFiles();
|
||||||
banks = processor.getBankFiles();
|
banks = processor.getBankFiles();
|
||||||
|
{
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::ImportPreset),
|
||||||
|
"Import Preset...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::ImportBank),
|
||||||
|
"Import Bank...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::ExportPreset),
|
||||||
|
"Export Preset...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::ExportBank),
|
||||||
|
"Export Bank...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::SavePreset),
|
||||||
|
"Save Preset...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::RenamePreset),
|
||||||
|
"Rename Preset...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::NewPreset),
|
||||||
|
"New Preset...",
|
||||||
|
true,//enableNewPresetOption,
|
||||||
|
false);
|
||||||
|
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::DeletePreset),
|
||||||
|
"Delete Preset...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
|
||||||
|
/*
|
||||||
|
fileMenu.addItem(static_cast<int>(MenuAction::DeleteBank),
|
||||||
|
"Delete Bank...",
|
||||||
|
true,
|
||||||
|
false);
|
||||||
|
*/
|
||||||
|
menu->addSubMenu("File", fileMenu);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < processor.getNumPrograms(); ++i)
|
for (int i = 0; i < processor.getNumPrograms(); ++i)
|
||||||
|
@ -463,8 +539,180 @@ void ObxdAudioProcessorEditor::resultFromMenu (const Point<int> pos)
|
||||||
clean();
|
clean();
|
||||||
loadSkin (processor);
|
loadSkin (processor);
|
||||||
}
|
}
|
||||||
|
else if (result < progStart){
|
||||||
|
MenuActionCallback(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessorEditor::MenuActionCallback(int action){
|
||||||
|
|
||||||
|
|
||||||
|
if (action == MenuAction::ImportBank)
|
||||||
|
{
|
||||||
|
fileChooser = std::make_unique<juce::FileChooser>("Import Bank (*.fxb)", juce::File(), "*.fxb", true);
|
||||||
|
|
||||||
|
if (fileChooser->browseForFileToOpen()) {
|
||||||
|
File result = fileChooser->getResult();
|
||||||
|
auto name = result.getFileName().replace("%20", " ");
|
||||||
|
auto file = processor.getBanksFolder().getChildFile(name);
|
||||||
|
|
||||||
|
if (result == file || result.copyFileTo(file)){
|
||||||
|
processor.loadFromFXBFile(file);
|
||||||
|
processor.scanAndUpdateBanks();
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action == MenuAction::ExportBank)
|
||||||
|
{
|
||||||
|
auto file = processor.getDocumentFolder().getChildFile("Banks");
|
||||||
|
FileChooser myChooser ("Export Bank (*.fxb)", file, "*.fxb", true);
|
||||||
|
if(myChooser.browseForFileToSave(true))
|
||||||
|
{
|
||||||
|
File result = myChooser.getResult();
|
||||||
|
|
||||||
|
String temp = result.getFullPathName();
|
||||||
|
if (!temp.endsWith(".fxb")) {
|
||||||
|
temp += ".fxb";
|
||||||
|
}
|
||||||
|
processor.saveBank(File(temp));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action == MenuAction::DeleteBank)
|
||||||
|
{
|
||||||
|
if(NativeMessageBox::showOkCancelBox(AlertWindow::NoIcon, "Delete Bank", "Delete current bank: " + processor.currentBank + "?")){
|
||||||
|
processor.deleteBank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (action == MenuAction::SavePreset)
|
||||||
|
{
|
||||||
|
auto presetName = processor.currentPreset;
|
||||||
|
if (presetName.isEmpty() )
|
||||||
|
{
|
||||||
|
processor.saveBank();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processor.savePreset();
|
||||||
|
processor.saveBank();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == MenuAction::NewPreset)
|
||||||
|
{
|
||||||
|
setPresetNameWindow = std::make_unique<SetPresetNameWindow>();
|
||||||
|
//preventBackgroundClick();
|
||||||
|
addAndMakeVisible(setPresetNameWindow.get());
|
||||||
|
resized();
|
||||||
|
|
||||||
|
auto callback = [this](int i, juce::String name)
|
||||||
|
{
|
||||||
|
if (i)
|
||||||
|
{
|
||||||
|
if (name.isNotEmpty())
|
||||||
|
{
|
||||||
|
processor.newPreset(name);
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPresetNameWindow.reset();
|
||||||
|
//preventBackgroundClickComponent.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
setPresetNameWindow->callback = callback;
|
||||||
|
setPresetNameWindow->grabTextEditorFocus();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == MenuAction::RenamePreset)
|
||||||
|
{
|
||||||
|
setPresetNameWindow = std::make_unique<SetPresetNameWindow>();
|
||||||
|
//preventBackgroundClick();
|
||||||
|
setPresetNameWindow->setText(processor.getProgramName(processor.getCurrentProgram()));
|
||||||
|
addAndMakeVisible(setPresetNameWindow.get());
|
||||||
|
resized();
|
||||||
|
|
||||||
|
auto callback = [this](int i, juce::String name)
|
||||||
|
{
|
||||||
|
if (i)
|
||||||
|
{
|
||||||
|
if (name.isNotEmpty())
|
||||||
|
{
|
||||||
|
processor.changePresetName(name);
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPresetNameWindow.reset();
|
||||||
|
//preventBackgroundClickComponent.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
setPresetNameWindow->callback = callback;
|
||||||
|
setPresetNameWindow->grabTextEditorFocus();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == MenuAction::DeletePreset)
|
||||||
|
{
|
||||||
|
if(NativeMessageBox::showOkCancelBox(AlertWindow::NoIcon, "Delete Preset", "Delete current preset " + processor.currentPreset + "?")){
|
||||||
|
processor.deletePreset();
|
||||||
|
createMenu();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (action == MenuAction::ImportPreset)
|
||||||
|
{
|
||||||
|
DBG("Import Preset");
|
||||||
|
fileChooser = std::make_unique<juce::FileChooser>("Import Preset (*.fxp)", juce::File(), "*.fxp", true);
|
||||||
|
|
||||||
|
if (fileChooser->browseForFileToOpen()) {
|
||||||
|
File result = fileChooser->getResult();
|
||||||
|
//auto name = result.getFileName().replace("%20", " ");
|
||||||
|
//auto file = processor.getPresetsFolder().getChildFile(name);
|
||||||
|
DBG("Import Preset: " << result.getFileName());
|
||||||
|
//if (result == file || result.copyFileTo(file)){
|
||||||
|
processor.loadPreset(result);
|
||||||
|
createMenu();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action == MenuAction::ExportPreset)
|
||||||
|
{
|
||||||
|
|
||||||
|
auto file = processor.getPresetsFolder();
|
||||||
|
FileChooser myChooser ("Export Preset (*.fxp)", file, "*.fxp", true);
|
||||||
|
if(myChooser.browseForFileToSave(true))
|
||||||
|
{
|
||||||
|
File result = myChooser.getResult();
|
||||||
|
|
||||||
|
String temp = result.getFullPathName();
|
||||||
|
if (!temp.endsWith(".fxp")) {
|
||||||
|
temp += ".fxp";
|
||||||
|
}
|
||||||
|
processor.savePreset(File(temp));
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void ObxdAudioProcessorEditor::deleteBank()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void ObxdAudioProcessorEditor::nextProgram() {
|
void ObxdAudioProcessorEditor::nextProgram() {
|
||||||
int cur = processor.getCurrentProgram() + 1;
|
int cur = processor.getCurrentProgram() + 1;
|
||||||
if (cur == processor.getNumPrograms()) {
|
if (cur == processor.getNumPrograms()) {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include "Gui/Knob.h"
|
#include "Gui/Knob.h"
|
||||||
#include "Gui/TooglableButton.h"
|
#include "Gui/TooglableButton.h"
|
||||||
#include "Gui/ButtonList.h"
|
#include "Gui/ButtonList.h"
|
||||||
|
#include "Components/SetPresetNameWindow.h"
|
||||||
|
|
||||||
enum KeyPressCommandIDs
|
enum KeyPressCommandIDs
|
||||||
{
|
{
|
||||||
|
@ -26,6 +27,23 @@ enum KeyPressCommandIDs
|
||||||
buttonPadPrevProgram,
|
buttonPadPrevProgram,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum MenuAction
|
||||||
|
{
|
||||||
|
Cancel = 0,
|
||||||
|
ToggleMidiKeyboard,
|
||||||
|
ImportPreset,
|
||||||
|
ImportBank,
|
||||||
|
ExportBank,
|
||||||
|
ExportPreset,
|
||||||
|
SavePreset,
|
||||||
|
NewPreset,
|
||||||
|
RenamePreset,
|
||||||
|
DeletePreset,
|
||||||
|
DeleteBank,
|
||||||
|
ShowBanks,
|
||||||
|
LoadBank // LoadBank must be the last enum value
|
||||||
|
};
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
|
@ -121,6 +139,11 @@ public:
|
||||||
|
|
||||||
void nextProgram();
|
void nextProgram();
|
||||||
void prevProgram();
|
void prevProgram();
|
||||||
|
|
||||||
|
void MenuActionCallback(int action);
|
||||||
|
void deleteBank();
|
||||||
|
|
||||||
|
void resized() override;
|
||||||
private:
|
private:
|
||||||
Knob* addKnob (int x, int y, int d, ObxdAudioProcessor& filter, int parameter, String name, float defval);
|
Knob* addKnob (int x, int y, int d, ObxdAudioProcessor& filter, int parameter, String name, float defval);
|
||||||
void placeLabel (int x, int y, String text);
|
void placeLabel (int x, int y, String text);
|
||||||
|
@ -227,7 +250,8 @@ private:
|
||||||
int skinStart;
|
int skinStart;
|
||||||
Array<File> skins;
|
Array<File> skins;
|
||||||
Array<File> banks;
|
Array<File> banks;
|
||||||
|
std::unique_ptr<SetPresetNameWindow> setPresetNameWindow;
|
||||||
|
std::unique_ptr<FileChooser> fileChooser;
|
||||||
// Command manager
|
// Command manager
|
||||||
ApplicationCommandManager commandManager;
|
ApplicationCommandManager commandManager;
|
||||||
};
|
};
|
||||||
|
|
|
@ -225,33 +225,6 @@ inline void ObxdAudioProcessor::processMidiPerSample (MidiBuffer::Iterator* iter
|
||||||
{
|
{
|
||||||
synth.procModWheel (midiMsg->getControllerValue() / 127.0f);
|
synth.procModWheel (midiMsg->getControllerValue() / 127.0f);
|
||||||
}
|
}
|
||||||
if (midiMsg->isController())
|
|
||||||
{
|
|
||||||
lastMovedController = midiMsg->getControllerNumber();
|
|
||||||
|
|
||||||
if (programs.currentProgramPtr->values[MIDILEARN] > 0.5f){
|
|
||||||
midiControlledParamSet = true;
|
|
||||||
bindings[lastMovedController] = lastUsedParameter;
|
|
||||||
setEngineParameterValue (MIDILEARN, 0, true);
|
|
||||||
lastMovedController = 0;
|
|
||||||
lastUsedParameter = 0;
|
|
||||||
midiControlledParamSet = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bindings[lastMovedController] > 0)
|
|
||||||
{
|
|
||||||
midiControlledParamSet = true;
|
|
||||||
setEngineParameterValue (bindings[lastMovedController],
|
|
||||||
midiMsg->getControllerValue() / 127.0f, true);
|
|
||||||
|
|
||||||
setEngineParameterValue (MIDILEARN, 0, true);
|
|
||||||
lastMovedController = 0;
|
|
||||||
lastUsedParameter = 0;
|
|
||||||
|
|
||||||
midiControlledParamSet = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if(midiMsg->isSustainPedalOn())
|
if(midiMsg->isSustainPedalOn())
|
||||||
{
|
{
|
||||||
synth.sustainOn();
|
synth.sustainOn();
|
||||||
|
@ -267,20 +240,39 @@ inline void ObxdAudioProcessor::processMidiPerSample (MidiBuffer::Iterator* iter
|
||||||
if(midiMsg->isAllSoundOff())
|
if(midiMsg->isAllSoundOff())
|
||||||
{
|
{
|
||||||
synth.allSoundOff();
|
synth.allSoundOff();
|
||||||
}
|
|
||||||
char* midi_data = (char*)midiMsg->getRawData();
|
|
||||||
int const status = midi_data[0] & 0xF0;
|
|
||||||
if (status == 0xC0)
|
|
||||||
{
|
|
||||||
{
|
|
||||||
//const ScopedUnlock unlocker(criticalSection);
|
|
||||||
// TODO - must issue setProgram
|
|
||||||
setCurrentProgram(midi_data[1]);
|
|
||||||
}
|
|
||||||
//sendChangeMessage();
|
|
||||||
//updateHostDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DBG(" Message: " << midiMsg->getChannel() << " "<<midiMsg->getRawData()[0] << " "<< midiMsg->getRawData()[1] << " "<< midiMsg->getRawData()[2]);
|
||||||
|
|
||||||
|
if (midiMsg->isProgramChange()){ // xC0
|
||||||
|
setCurrentProgram(midiMsg->getProgramChangeNumber());
|
||||||
|
|
||||||
|
} else
|
||||||
|
if (midiMsg->isController()) // xB0
|
||||||
|
{
|
||||||
|
lastMovedController = midiMsg->getControllerNumber();
|
||||||
|
if (programs.currentProgramPtr->values[MIDILEARN] > 0.5f){
|
||||||
|
midiControlledParamSet = true;
|
||||||
|
bindings[lastMovedController] = lastUsedParameter;
|
||||||
|
setEngineParameterValue (MIDILEARN, 0, true);
|
||||||
|
lastMovedController = 0;
|
||||||
|
lastUsedParameter = 0;
|
||||||
|
midiControlledParamSet = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindings[lastMovedController] > 0)
|
||||||
|
{
|
||||||
|
midiControlledParamSet = true;
|
||||||
|
setEngineParameterValue (bindings[lastMovedController],
|
||||||
|
midiMsg->getControllerValue() / 127.0f, true);
|
||||||
|
|
||||||
|
setEngineParameterValue (MIDILEARN, 0, true);
|
||||||
|
lastMovedController = 0;
|
||||||
|
lastUsedParameter = 0;
|
||||||
|
|
||||||
|
midiControlledParamSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,6 +461,119 @@ void ObxdAudioProcessor::setCurrentProgramStateInformation(const void* data, in
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
bool ObxdAudioProcessor::deleteBank() {
|
||||||
|
currentBankFile.deleteFile();
|
||||||
|
scanAndUpdateBanks();
|
||||||
|
if (bankFiles.size() > 0)
|
||||||
|
{
|
||||||
|
loadFromFXBFile (bankFiles[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessor::saveBank() {
|
||||||
|
saveFXBFile(currentBankFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObxdAudioProcessor::loadPreset(const File& fxpFile) {
|
||||||
|
loadFromFXBFile(fxpFile);
|
||||||
|
currentPreset = fxpFile.getFileName();
|
||||||
|
currentPresetFile = fxpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObxdAudioProcessor::saveFXPFile(const File& fxpFile){
|
||||||
|
//auto xml = std::unique_ptr<juce::XmlElement>(new juce::XmlElement(""));
|
||||||
|
juce::MemoryBlock m, memoryBlock;
|
||||||
|
getCurrentProgramStateInformation(m);
|
||||||
|
{
|
||||||
|
memoryBlock.reset();
|
||||||
|
auto totalLen = sizeof (fxProgramSet) + m.getSize() - 8;
|
||||||
|
memoryBlock.setSize (totalLen, true);
|
||||||
|
|
||||||
|
auto set = static_cast<fxProgramSet*>(memoryBlock.getData());
|
||||||
|
set->chunkMagic = fxbName ("CcnK");
|
||||||
|
set->byteSize = 0;
|
||||||
|
set->fxMagic = fxbName ("FPCh");
|
||||||
|
set->version = fxbSwap (fxbVersionNum);
|
||||||
|
set->fxID = fxbName ("Obxd");
|
||||||
|
set->fxVersion = fxbSwap (fxbVersionNum);
|
||||||
|
set->numPrograms = fxbSwap (getNumPrograms());
|
||||||
|
programs.currentProgramPtr->name.copyToUTF8(set->name, 28);
|
||||||
|
set->chunkSize = fxbSwap (static_cast<int32>(m.getSize()));
|
||||||
|
|
||||||
|
m.copyTo (set->chunk, 0, m.getSize());
|
||||||
|
|
||||||
|
fxpFile.replaceWithData(memoryBlock.getData(), memoryBlock.getSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObxdAudioProcessor::savePreset(const File& fxpFile) {
|
||||||
|
saveFXPFile(fxpFile);
|
||||||
|
currentPreset = fxpFile.getFileName();
|
||||||
|
currentPresetFile = fxpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessor::changePresetName(const String &name){
|
||||||
|
programs.currentProgramPtr->name = name;
|
||||||
|
//savePreset();
|
||||||
|
saveBank();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessor::deletePreset(){
|
||||||
|
programs.currentProgramPtr->setDefaultValues();
|
||||||
|
programs.currentProgramPtr->name = "Default";
|
||||||
|
saveBank();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessor::newPreset(const String &name) {
|
||||||
|
for (int i = 0; i < PROGRAMCOUNT; ++i)
|
||||||
|
{
|
||||||
|
if (programs.programs[i].name == "Default"){
|
||||||
|
setCurrentProgram(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//savePreset();
|
||||||
|
saveBank();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObxdAudioProcessor::savePreset() {
|
||||||
|
savePreset(currentPresetFile);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObxdAudioProcessor::saveBank(const File& fxbFile){
|
||||||
|
saveFXBFile(fxbFile);
|
||||||
|
currentBankFile = fxbFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ObxdAudioProcessor::saveFXBFile(const File& fxbFile) {
|
||||||
|
//auto xml = std::unique_ptr<juce::XmlElement>(new juce::XmlElement(""));
|
||||||
|
juce::MemoryBlock m, memoryBlock;
|
||||||
|
getStateInformation(m);
|
||||||
|
|
||||||
|
{
|
||||||
|
memoryBlock.reset();
|
||||||
|
auto totalLen = sizeof (fxChunkSet) + m.getSize() - 8;
|
||||||
|
memoryBlock.setSize (totalLen, true);
|
||||||
|
|
||||||
|
auto set = static_cast<fxChunkSet*>( memoryBlock.getData());
|
||||||
|
set->chunkMagic = fxbName ("CcnK");
|
||||||
|
set->byteSize = 0;
|
||||||
|
set->fxMagic = fxbName ("FBCh");
|
||||||
|
set->version = fxbSwap (fxbVersionNum);
|
||||||
|
set->fxID = fxbName ("Obxd");
|
||||||
|
set->fxVersion = fxbSwap (fxbVersionNum);
|
||||||
|
set->numPrograms = fxbSwap (getNumPrograms());
|
||||||
|
set->chunkSize = fxbSwap (static_cast<int32>(m.getSize()));
|
||||||
|
|
||||||
|
m.copyTo (set->chunk, 0, m.getSize());
|
||||||
|
fxbFile.replaceWithData(memoryBlock.getData(), memoryBlock.getSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ObxdAudioProcessor::loadFromFXBFile(const File& fxbFile)
|
bool ObxdAudioProcessor::loadFromFXBFile(const File& fxbFile)
|
||||||
{
|
{
|
||||||
MemoryBlock mb;
|
MemoryBlock mb;
|
||||||
|
@ -563,7 +668,7 @@ bool ObxdAudioProcessor::loadFromFXBFile(const File& fxbFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentBank = fxbFile.getFileName();
|
currentBank = fxbFile.getFileName();
|
||||||
|
currentBankFile = fxbFile;
|
||||||
updateHostDisplay();
|
updateHostDisplay();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -588,13 +693,14 @@ bool ObxdAudioProcessor::restoreProgramSettings(const fxProgram* const prog)
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
void ObxdAudioProcessor::scanAndUpdateBanks()
|
void ObxdAudioProcessor::scanAndUpdateBanks()
|
||||||
{
|
{
|
||||||
bankFiles.clearQuick();
|
bankFiles.clear();
|
||||||
|
|
||||||
DirectoryIterator it (getBanksFolder(), false, "*.fxb", File::findFiles);
|
DirectoryIterator it (getBanksFolder(), false, "*.fxb", File::findFiles);
|
||||||
|
|
||||||
while (it.next())
|
while (it.next())
|
||||||
{
|
{
|
||||||
bankFiles.addUsingDefaultSort (it.getFile());
|
bankFiles.addUsingDefaultSort (it.getFile());
|
||||||
|
DBG("Scan Banks: " << it.getFile().getFullPathName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,6 +756,11 @@ File ObxdAudioProcessor::getBanksFolder() const
|
||||||
return getDocumentFolder().getChildFile("Banks");
|
return getDocumentFolder().getChildFile("Banks");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File ObxdAudioProcessor::getPresetsFolder() const
|
||||||
|
{
|
||||||
|
return getDocumentFolder().getChildFile("Presets");
|
||||||
|
}
|
||||||
|
|
||||||
File ObxdAudioProcessor::getCurrentSkinFolder() const
|
File ObxdAudioProcessor::getCurrentSkinFolder() const
|
||||||
{
|
{
|
||||||
return getSkinFolder().getChildFile(currentSkin);
|
return getSkinFolder().getChildFile(currentSkin);
|
||||||
|
@ -783,6 +894,7 @@ void ObxdAudioProcessor::setEngineParameterValue (int index, float newValue, boo
|
||||||
apvtState.getParameter(getEngineParameterId(index))->setValue(newValue);
|
apvtState.getParameter(getEngineParameterId(index))->setValue(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//DBG("Set Value Parameter: " << getEngineParameterId(index) << " Val: " << newValue);
|
||||||
switch (index)
|
switch (index)
|
||||||
{
|
{
|
||||||
case SELF_OSC_PUSH:
|
case SELF_OSC_PUSH:
|
||||||
|
|
|
@ -167,7 +167,17 @@ public:
|
||||||
void scanAndUpdateSkins();
|
void scanAndUpdateSkins();
|
||||||
const Array<File>& getBankFiles() const;
|
const Array<File>& getBankFiles() const;
|
||||||
const Array<File>& getSkinFiles() const;
|
const Array<File>& getSkinFiles() const;
|
||||||
|
bool deleteBank();
|
||||||
|
bool loadPreset(const File& fxpFile);
|
||||||
|
bool savePreset(const File& fxpFile);
|
||||||
|
void changePresetName(const String &name);
|
||||||
|
void newPreset(const String &name);
|
||||||
|
void deletePreset();
|
||||||
|
|
||||||
bool loadFromFXBFile(const File& fxbFile);
|
bool loadFromFXBFile(const File& fxbFile);
|
||||||
|
bool saveFXBFile(const File& fxbFile);
|
||||||
|
bool saveFXPFile(const File& fxpFile);
|
||||||
|
bool saveBank(const File& fxbFile);
|
||||||
bool restoreProgramSettings(const fxProgram* const prog);
|
bool restoreProgramSettings(const fxProgram* const prog);
|
||||||
File getCurrentBankFile() const;
|
File getCurrentBankFile() const;
|
||||||
|
|
||||||
|
@ -178,6 +188,7 @@ public:
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
File getDocumentFolder() const;
|
File getDocumentFolder() const;
|
||||||
File getSkinFolder() const;
|
File getSkinFolder() const;
|
||||||
|
File getPresetsFolder() const;
|
||||||
File getBanksFolder() const;
|
File getBanksFolder() const;
|
||||||
|
|
||||||
File getCurrentSkinFolder() const;
|
File getCurrentSkinFolder() const;
|
||||||
|
@ -209,7 +220,16 @@ private:
|
||||||
ObxdBank programs;
|
ObxdBank programs;
|
||||||
|
|
||||||
String currentSkin;
|
String currentSkin;
|
||||||
|
public:
|
||||||
String currentBank;
|
String currentBank;
|
||||||
|
File currentBankFile;
|
||||||
|
void saveBank();
|
||||||
|
|
||||||
|
|
||||||
|
String currentPreset;
|
||||||
|
File currentPresetFile;
|
||||||
|
void savePreset();
|
||||||
|
private:
|
||||||
Array<File> bankFiles;
|
Array<File> bankFiles;
|
||||||
Array<File> skinFiles;
|
Array<File> skinFiles;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue