decoupling achieved. compiles. doesn't immediately explode. but neither banks nor presets visible.
This commit is contained in:
parent
58574425f3
commit
8c1be957fe
|
@ -7,8 +7,8 @@
|
|||
#include "Util.h"
|
||||
|
||||
FilePicker::FilePicker(
|
||||
AudioProcessorValueTreeState& valueTreeState,
|
||||
FluidSynthModel& fluidSynthModel
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
// FluidSynthModel& fluidSynthModel
|
||||
)
|
||||
: fileChooser{
|
||||
"File",
|
||||
|
@ -20,7 +20,7 @@ FilePicker::FilePicker(
|
|||
String(),
|
||||
"Choose a Soundfont file to load into the synthesizer"}
|
||||
, valueTreeState{valueTreeState}
|
||||
, fluidSynthModel{fluidSynthModel}
|
||||
// , fluidSynthModel{fluidSynthModel}
|
||||
// , currentPath{}
|
||||
{
|
||||
// faster (rounded edges introduce transparency)
|
||||
|
|
|
@ -15,8 +15,8 @@ class FilePicker: public Component,
|
|||
{
|
||||
public:
|
||||
FilePicker(
|
||||
AudioProcessorValueTreeState& valueTreeState,
|
||||
FluidSynthModel& fluidSynthModel
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
// FluidSynthModel& fluidSynthModel
|
||||
);
|
||||
~FilePicker();
|
||||
|
||||
|
@ -41,7 +41,7 @@ private:
|
|||
FilenameComponent fileChooser;
|
||||
|
||||
AudioProcessorValueTreeState& valueTreeState;
|
||||
FluidSynthModel& fluidSynthModel;
|
||||
// FluidSynthModel& fluidSynthModel;
|
||||
|
||||
String currentPath;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "Pills.h"
|
||||
#include "MyColours.h"
|
||||
#include <algorithm>
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
@ -14,7 +15,8 @@ Pill::Pill(
|
|||
bool isLast
|
||||
)
|
||||
// : pills{pills}
|
||||
: bank{bank}
|
||||
: valueTreeState{valueTreeState}
|
||||
, bank{bank}
|
||||
, textButton{String(bank)}
|
||||
{
|
||||
textButton.setConnectedEdges (
|
||||
|
@ -25,16 +27,18 @@ Pill::Pill(
|
|||
loadToggleState();
|
||||
textButton.setClickingTogglesState(true);
|
||||
|
||||
valueTreeState.state.addListener(this);
|
||||
valueTreeState.addParameterListener("bank", this);
|
||||
// valueTreeState.state.addListener(this);
|
||||
textButton.addListener(this);
|
||||
}
|
||||
|
||||
Pill::~Pill() {
|
||||
valueTreeState.state.removeListener(this);
|
||||
valueTreeState.removeParameterListener("bank", this);
|
||||
// valueTreeState.state.removeListener(this);
|
||||
textButton.removeListener(this);
|
||||
}
|
||||
|
||||
Pill::loadToggleState() {
|
||||
void Pill::loadToggleState() {
|
||||
RangedAudioParameter *param {valueTreeState.getParameter("bank")};
|
||||
jassert(dynamic_cast<AudioParameterInt*> (param) != nullptr);
|
||||
AudioParameterInt* castParam {dynamic_cast<AudioParameterInt*> (param)};
|
||||
|
@ -57,13 +61,13 @@ void Pill::parameterChanged(const String& parameterID, float newValue) {
|
|||
}
|
||||
}
|
||||
|
||||
void Pill::valueTreePropertyChanged(
|
||||
ValueTree& treeWhosePropertyHasChanged,
|
||||
const Identifier& property) {
|
||||
if (treeWhosePropertyHasChanged.getType() == StringRef("presets")) {
|
||||
loadModelFrom(treeWhosePropertyHasChanged);
|
||||
}
|
||||
}
|
||||
// void Pill::valueTreePropertyChanged(
|
||||
// ValueTree& treeWhosePropertyHasChanged,
|
||||
// const Identifier& property) {
|
||||
// if (treeWhosePropertyHasChanged.getType() == StringRef("presets")) {
|
||||
// loadModelFrom(treeWhosePropertyHasChanged);
|
||||
// }
|
||||
// }
|
||||
|
||||
Pills::Pills(
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
|
@ -83,7 +87,8 @@ Pills::Pills(
|
|||
setOpaque (true);
|
||||
|
||||
// populate(initiallySelectedItem);
|
||||
loadModelFrom(valueTreeState.state.getChildWithName("banks"));
|
||||
ValueTree banks{valueTreeState.state.getChildWithName("banks")};
|
||||
loadModelFrom(banks);
|
||||
|
||||
valueTreeState.state.addListener(this);
|
||||
}
|
||||
|
@ -109,11 +114,12 @@ void Pills::loadModelFrom(ValueTree& banks) {
|
|||
// rows.push_back(unique_ptr<Pill>(new Pill(), [](Pill* pill) {
|
||||
// pill->remo
|
||||
// }));
|
||||
pills.emplace_back(
|
||||
this.valueTreeState,
|
||||
pills.push_back(
|
||||
make_unique<Pill>(
|
||||
valueTreeState,
|
||||
num,
|
||||
i == 0,
|
||||
i == numChildren - 1);
|
||||
i == numChildren - 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,21 +166,51 @@ void Pills::loadModelFrom(ValueTree& banks) {
|
|||
// }
|
||||
|
||||
void Pills::cycle(bool right) {
|
||||
// TODO: base this on valueTree
|
||||
int currentIx = static_cast<const int>(distance(pills.begin(), find(pills.begin(), pills.end(), selected)));
|
||||
RangedAudioParameter *param {valueTreeState.getParameter("bank")};
|
||||
jassert(dynamic_cast<AudioParameterInt*> (param) != nullptr);
|
||||
AudioParameterInt* castParam {dynamic_cast<AudioParameterInt*> (param)};
|
||||
int currentlySelectedBank{castParam->get()};
|
||||
|
||||
ValueTree banks{valueTreeState.state.getChildWithName("banks")};
|
||||
// int numChildren{banks.getNumChildren()};
|
||||
|
||||
vector<int> bankInts;
|
||||
bankInts.resize(banks.getNumChildren());
|
||||
|
||||
transform(banks.begin(), banks.end(), bankInts.begin(), [](ValueTree bank) -> int {
|
||||
return bank.getProperty("num");
|
||||
});
|
||||
|
||||
// int closestBank{currentlySelectedBank};
|
||||
// for(int i{0}; i < numChildren; i++) {
|
||||
// ValueTree child{banks.getChild(i)};
|
||||
// int proposedBank{child.getProperty("num")};
|
||||
// if (right && proposedBank > currentlySelectedBank) {
|
||||
// closestBank = jmin(closestBank, proposedBank);
|
||||
// } else if (left )
|
||||
// }
|
||||
|
||||
int currentIx{static_cast<const int>(distance(bankInts.begin(), find(bankInts.begin(), bankInts.end(), currentlySelectedBank)))};
|
||||
currentIx += right ? 1 : pills.size()-1;
|
||||
pills[currentIx % pills.size()]->textButton.triggerClick();
|
||||
// pills[currentIx % pills.size()]->textButton.triggerClick();
|
||||
*castParam = bankInts[currentIx % bankInts.size()];
|
||||
|
||||
|
||||
// TODO: base this on valueTree
|
||||
// int currentIx = static_cast<const int>(distance(pills.begin(), find(pills.begin(), pills.end(), selected)));
|
||||
// currentIx += right ? 1 : pills.size()-1;
|
||||
// pills[currentIx % pills.size()]->textButton.triggerClick();
|
||||
}
|
||||
|
||||
void Pills::resized() {
|
||||
int index = 0;
|
||||
Rectangle<int> r (getLocalBounds());
|
||||
const int equalWidth = r.proportionOfWidth(buttons.size() <= 0 ? 1.0 : 1.0f/buttons.size());
|
||||
for(TextButton* t : buttons) {
|
||||
const int equalWidth = r.proportionOfWidth(pills.size() <= 0 ? 1.0 : 1.0f/pills.size());
|
||||
for(auto& pill : pills) {
|
||||
Rectangle<int> r2 (getLocalBounds());
|
||||
r2.removeFromLeft(equalWidth * index);
|
||||
r2.removeFromRight(equalWidth * (buttons.size()-index-1));
|
||||
t->setBounds (r2);
|
||||
r2.removeFromRight(equalWidth * (static_cast<int>(pills.size())-index-1));
|
||||
pill->textButton.setBounds (r2);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,7 @@ private:
|
|||
TextButton textButton;
|
||||
|
||||
friend class Pills;
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pill)
|
||||
}
|
||||
};
|
||||
|
||||
class Pills
|
||||
: public Component
|
||||
|
|
|
@ -21,8 +21,8 @@ JuicySFAudioProcessorEditor::JuicySFAudioProcessorEditor(
|
|||
, valueTreeState{valueTreeState}
|
||||
// sharedParams{p.getSharedParams()},
|
||||
, midiKeyboard{p.keyboardState, SurjectiveMidiKeyboardComponent::horizontalKeyboard}
|
||||
, tablesComponent{valueTreeState, p.getFluidSynthModel()}
|
||||
, filePicker{valueTreeState, p.getFluidSynthModel()}
|
||||
, tablesComponent{valueTreeState}
|
||||
, filePicker{valueTreeState}
|
||||
, slidersComponent{valueTreeState, p.getFluidSynthModel()}
|
||||
{
|
||||
// set resize limits for this plug-in
|
||||
|
|
|
@ -17,7 +17,7 @@ using namespace Util;
|
|||
This class shows how to implement a TableListBoxModel to show in a TableListBox.
|
||||
*/
|
||||
TableComponent::TableComponent(
|
||||
AudioProcessorValueTreeState& valueTreeState,
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
// const vector<string> &columns,
|
||||
// const vector<TableRow> &rows,
|
||||
// const function<void (int)> &onRowSelected,
|
||||
|
@ -74,7 +74,8 @@ TableComponent::TableComponent(
|
|||
table.setWantsKeyboardFocus(false);
|
||||
|
||||
// table.selectRow();
|
||||
loadModelFrom(valueTreeState.state.getChildWithName("presets"));
|
||||
ValueTree presets{valueTreeState.state.getChildWithName("presets")};
|
||||
loadModelFrom(presets);
|
||||
// selectCurrentPreset();
|
||||
|
||||
// we could now change some initial settings..
|
||||
|
@ -160,9 +161,9 @@ void TableComponent::paintRowBackground (
|
|||
|
||||
String TableRow::getStringContents(int columnId) {
|
||||
if (columnId <= 1) {
|
||||
return String(row.preset);
|
||||
return String(preset);
|
||||
}
|
||||
return row.name;
|
||||
return name;
|
||||
}
|
||||
|
||||
// This is overloaded from TableListBoxModel, and must paint any cells that aren't using custom
|
||||
|
|
|
@ -29,7 +29,6 @@ private:
|
|||
String name;
|
||||
|
||||
friend class TableComponent;
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableRow)
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -8,105 +8,107 @@ using namespace std;
|
|||
using namespace placeholders;
|
||||
|
||||
TablesComponent::TablesComponent(
|
||||
AudioProcessorValueTreeState& valueTreeState,
|
||||
FluidSynthModel& fluidSynthModel
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
// FluidSynthModel& fluidSynthModel
|
||||
)
|
||||
: valueTreeState{valueTreeState}
|
||||
, fluidSynthModel{fluidSynthModel}
|
||||
, banksToPresets{fluidSynthModel.getBanks()}
|
||||
, initialised{false}
|
||||
// , fluidSynthModel{fluidSynthModel}
|
||||
, banks{valueTreeState}
|
||||
, presetTable{valueTreeState}
|
||||
// , banksToPresets{fluidSynthModel.getBanks()}
|
||||
// , initialised{false}
|
||||
{
|
||||
fluid_preset_t* currentPreset = getCurrentPreset();
|
||||
selectedBank = -1;
|
||||
int selectedPreset = -1;
|
||||
// fluid_preset_t* currentPreset = getCurrentPreset();
|
||||
// selectedBank = -1;
|
||||
// int selectedPreset = -1;
|
||||
|
||||
|
||||
|
||||
if (currentPreset != nullptr) {
|
||||
selectedBank = fluid_preset_get_banknum(currentPreset);
|
||||
selectedPreset = fluid_preset_get_num(currentPreset);
|
||||
}
|
||||
// if (currentPreset != nullptr) {
|
||||
// selectedBank = fluid_preset_get_banknum(currentPreset);
|
||||
// selectedPreset = fluid_preset_get_num(currentPreset);
|
||||
// }
|
||||
|
||||
// auto rowToPresetMapper = [this](const vector<string> &row) {
|
||||
// return stoi(row[0]);
|
||||
// };
|
||||
auto itemToBankMapper = [](const string &item) {
|
||||
return stoi(item);
|
||||
};
|
||||
// auto itemToBankMapper = [](const string &item) {
|
||||
// return stoi(item);
|
||||
// };
|
||||
|
||||
presetTable = new TableComponent(
|
||||
valueTreeState,
|
||||
// {"#", "Name"},
|
||||
// mapPresets(
|
||||
// banksToPresets,
|
||||
// selectedBank
|
||||
// ),
|
||||
// [this](int preset){
|
||||
// this->onPresetSelected(preset);
|
||||
// presetTable = new TableComponent(
|
||||
// valueTreeState,
|
||||
// // {"#", "Name"},
|
||||
// // mapPresets(
|
||||
// // banksToPresets,
|
||||
// // selectedBank
|
||||
// // ),
|
||||
// // [this](int preset){
|
||||
// // this->onPresetSelected(preset);
|
||||
// // },
|
||||
// // rowToPresetMapper,
|
||||
// // presetToIndexMapper(selectedPreset)
|
||||
// );
|
||||
// banks = new Pills(
|
||||
// "Banks",
|
||||
// mapBanks(banksToPresets),
|
||||
// [this](int bank){
|
||||
// this->onBankSelected(bank);
|
||||
// },
|
||||
// rowToPresetMapper,
|
||||
// presetToIndexMapper(selectedPreset)
|
||||
);
|
||||
banks = new Pills(
|
||||
"Banks",
|
||||
mapBanks(banksToPresets),
|
||||
[this](int bank){
|
||||
this->onBankSelected(bank);
|
||||
},
|
||||
itemToBankMapper,
|
||||
selectedBank
|
||||
);
|
||||
// itemToBankMapper,
|
||||
// selectedBank
|
||||
// );
|
||||
|
||||
presetTable->setWantsKeyboardFocus(false);
|
||||
presetTable.setWantsKeyboardFocus(false);
|
||||
|
||||
addAndMakeVisible(presetTable);
|
||||
|
||||
addAndMakeVisible(banks);
|
||||
|
||||
initialised = true;
|
||||
// initialised = true;
|
||||
|
||||
fluidSynthModel.addListener(this);
|
||||
// fluidSynthModel.addListener(this);
|
||||
}
|
||||
|
||||
fluid_preset_t* TablesComponent::getCurrentPreset() {
|
||||
shared_ptr<fluid_synth_t> synth {fluidSynthModel.getSynth()};
|
||||
// fluid_preset_t* TablesComponent::getCurrentPreset() {
|
||||
// shared_ptr<fluid_synth_t> synth {fluidSynthModel.getSynth()};
|
||||
|
||||
return fluid_synth_get_channel_preset(synth.get(), fluidSynthModel.getChannel());
|
||||
}
|
||||
// return fluid_synth_get_channel_preset(synth.get(), fluidSynthModel.getChannel());
|
||||
// }
|
||||
|
||||
Preset TablesComponent::getFirstPresetInBank(int bank) {
|
||||
pair<BanksToPresets::const_iterator, BanksToPresets::const_iterator> iterators = banksToPresets.equal_range(bank);
|
||||
BanksToPresets::const_iterator it = iterators.first;
|
||||
return it->second;
|
||||
}
|
||||
// Preset TablesComponent::getFirstPresetInBank(int bank) {
|
||||
// pair<BanksToPresets::const_iterator, BanksToPresets::const_iterator> iterators = banksToPresets.equal_range(bank);
|
||||
// BanksToPresets::const_iterator it = iterators.first;
|
||||
// return it->second;
|
||||
// }
|
||||
|
||||
void TablesComponent::onBankSelected(int bank) {
|
||||
if (!initialised || bank == -1) {
|
||||
return;
|
||||
}
|
||||
cout << "Bank " << bank << endl;
|
||||
selectedBank = bank;
|
||||
Preset firstPresetInBank = getFirstPresetInBank(bank);
|
||||
presetTable->setRows(
|
||||
mapPresets(
|
||||
banksToPresets,
|
||||
bank
|
||||
),
|
||||
presetToIndexMapper(firstPresetInBank.getPreset())
|
||||
);
|
||||
}
|
||||
// void TablesComponent::onBankSelected(int bank) {
|
||||
// if (!initialised || bank == -1) {
|
||||
// return;
|
||||
// }
|
||||
// cout << "Bank " << bank << endl;
|
||||
// selectedBank = bank;
|
||||
// Preset firstPresetInBank = getFirstPresetInBank(bank);
|
||||
// presetTable->setRows(
|
||||
// mapPresets(
|
||||
// banksToPresets,
|
||||
// bank
|
||||
// ),
|
||||
// presetToIndexMapper(firstPresetInBank.getPreset())
|
||||
// );
|
||||
// }
|
||||
|
||||
int TablesComponent::presetToIndexMapper(int preset) {
|
||||
int ix = 0;
|
||||
pair<BanksToPresets::const_iterator, BanksToPresets::const_iterator> iterators = this->banksToPresets.equal_range(this->selectedBank);
|
||||
for (auto it = iterators.first; it != iterators.second; ++it, ix++) {
|
||||
Preset b = it->second;
|
||||
if (preset == b.getPreset()) {
|
||||
return ix;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// int TablesComponent::presetToIndexMapper(int preset) {
|
||||
// int ix = 0;
|
||||
// pair<BanksToPresets::const_iterator, BanksToPresets::const_iterator> iterators = this->banksToPresets.equal_range(this->selectedBank);
|
||||
// for (auto it = iterators.first; it != iterators.second; ++it, ix++) {
|
||||
// Preset b = it->second;
|
||||
// if (preset == b.getPreset()) {
|
||||
// return ix;
|
||||
// }
|
||||
// }
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
// void TablesComponent::onPresetSelected(int preset) {
|
||||
// if (!initialised || preset == -1) {
|
||||
|
@ -117,25 +119,25 @@ int TablesComponent::presetToIndexMapper(int preset) {
|
|||
// fluidSynthModel.changePreset(selectedBank, preset);
|
||||
// }
|
||||
|
||||
TablesComponent::~TablesComponent() {
|
||||
delete presetTable;
|
||||
delete banks;
|
||||
// fluidSynthModel.removeListener(this);
|
||||
}
|
||||
// TablesComponent::~TablesComponent() {
|
||||
// delete presetTable;
|
||||
// delete banks;
|
||||
// // fluidSynthModel.removeListener(this);
|
||||
// }
|
||||
|
||||
vector<string> TablesComponent::mapBanks(const BanksToPresets &banksToPresets) {
|
||||
vector<string> rows;
|
||||
// vector<string> TablesComponent::mapBanks(const BanksToPresets &banksToPresets) {
|
||||
// vector<string> rows;
|
||||
|
||||
const auto compareKey = [](const BanksToPresets::value_type& lhs, const BanksToPresets::value_type& rhs) {
|
||||
return lhs.first < rhs.first;
|
||||
};
|
||||
// const auto compareKey = [](const BanksToPresets::value_type& lhs, const BanksToPresets::value_type& rhs) {
|
||||
// return lhs.first < rhs.first;
|
||||
// };
|
||||
|
||||
for(auto i = banksToPresets.begin(); i != banksToPresets.end(); i = std::upper_bound(i, banksToPresets.end(), *i, compareKey)) {
|
||||
rows.push_back(to_string(i->first));
|
||||
}
|
||||
// for(auto i = banksToPresets.begin(); i != banksToPresets.end(); i = std::upper_bound(i, banksToPresets.end(), *i, compareKey)) {
|
||||
// rows.push_back(to_string(i->first));
|
||||
// }
|
||||
|
||||
return rows;
|
||||
}
|
||||
// return rows;
|
||||
// }
|
||||
|
||||
|
||||
// vector<vector<string>> TablesComponent::mapPresets(const BanksToPresets &banksToPresets, int bank) {
|
||||
|
@ -156,18 +158,18 @@ vector<string> TablesComponent::mapBanks(const BanksToPresets &banksToPresets) {
|
|||
|
||||
void TablesComponent::resized() {
|
||||
Rectangle<int> r (getLocalBounds());
|
||||
banks->setBounds (r.removeFromTop(27).reduced(5,0));
|
||||
banks.setBounds (r.removeFromTop(27).reduced(5,0));
|
||||
|
||||
presetTable->setBounds (r);
|
||||
presetTable.setBounds (r);
|
||||
}
|
||||
|
||||
bool TablesComponent::keyPressed(const KeyPress &key) {
|
||||
if (key.getKeyCode() == KeyPress::leftKey
|
||||
|| key.getKeyCode() == KeyPress::rightKey) {
|
||||
banks->cycle(key.getKeyCode() == KeyPress::rightKey);
|
||||
banks.cycle(key.getKeyCode() == KeyPress::rightKey);
|
||||
return true;
|
||||
}
|
||||
return presetTable->keyPressed(key);
|
||||
return presetTable.keyPressed(key);
|
||||
}
|
||||
|
||||
// void TablesComponent::fontChanged(FluidSynthModel *, const String &) {
|
||||
|
|
|
@ -20,10 +20,10 @@ class TablesComponent : public Component/*,
|
|||
{
|
||||
public:
|
||||
TablesComponent(
|
||||
AudioProcessorValueTreeState& valueTreeState,
|
||||
FluidSynthModel& fluidSynthModel
|
||||
AudioProcessorValueTreeState& valueTreeState
|
||||
// FluidSynthModel& fluidSynthModel
|
||||
);
|
||||
~TablesComponent();
|
||||
// ~TablesComponent();
|
||||
|
||||
void resized() override;
|
||||
|
||||
|
@ -32,25 +32,25 @@ public:
|
|||
|
||||
private:
|
||||
AudioProcessorValueTreeState& valueTreeState;
|
||||
FluidSynthModel& fluidSynthModel;
|
||||
int selectedBank;
|
||||
// FluidSynthModel& fluidSynthModel;
|
||||
// int selectedBank;
|
||||
|
||||
Pills banks;
|
||||
TableComponent presetTable;
|
||||
|
||||
BanksToPresets banksToPresets;
|
||||
// BanksToPresets banksToPresets;
|
||||
|
||||
// static vector<vector<string>> mapPresets(const BanksToPresets &banksToPresets, int bank);
|
||||
static vector<string> mapBanks(const BanksToPresets &banksToPresets);
|
||||
// static vector<string> mapBanks(const BanksToPresets &banksToPresets);
|
||||
|
||||
void onBankSelected(int bank);
|
||||
void onPresetSelected(int preset);
|
||||
int presetToIndexMapper(int preset);
|
||||
// void onBankSelected(int bank);
|
||||
// void onPresetSelected(int preset);
|
||||
// int presetToIndexMapper(int preset);
|
||||
|
||||
fluid_preset_t* getCurrentPreset();
|
||||
Preset getFirstPresetInBank(int bank);
|
||||
// fluid_preset_t* getCurrentPreset();
|
||||
// Preset getFirstPresetInBank(int bank);
|
||||
|
||||
bool initialised;
|
||||
// bool initialised;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TablesComponent)
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user