/*
// Copyright (c) 2021-2022 Timothy Schoen
// For information on usage and redistribution, and for a DISCLAIMER OF ALL
// WARRANTIES, see the file, "LICENSE.txt," in this distribution.
*/
#include "Components/PropertiesPanel.h"
extern "C" {
void garray_arraydialog(t_fake_garray* x, t_symbol* name, t_floatarg fsize, t_floatarg fflags, t_floatarg deleteit);
}
class GraphicalArray : public Component
, public Value::Listener
, public pd::MessageListener
, public NVGComponent {
public:
Object* object;
enum DrawType {
Points,
Polygon,
Curve
};
Value name = SynchronousValue();
Value size = SynchronousValue();
Value drawMode = SynchronousValue();
Value saveContents = SynchronousValue();
Value range = SynchronousValue();
bool visible = true;
std::function reloadGraphs = []() { };
GraphicalArray(PluginProcessor* instance, void* ptr, Object* parent)
: NVGComponent(this)
, object(parent)
, arr(ptr, instance)
, edited(false)
, pd(instance)
{
vec.reserve(8192);
read(vec);
updateParameters();
for (auto* value : SmallArray { &name, &size, &drawMode, &saveContents, &range }) {
// TODO: implement undo/redo for these values!
value->addListener(this);
}
pd->registerMessageListener(arr.getRawUnchecked(), this);
setInterceptsMouseClicks(true, false);
setOpaque(false);
}
~GraphicalArray()
{
pd->unregisterMessageListener(this);
}
static HeapArray rescale(HeapArray const& v, unsigned const newSize)
{
if (v.empty()) {
return {};
}
HeapArray result(newSize);
std::size_t const oldSize = v.size();
for (unsigned i = 0; i < newSize; i++) {
auto const idx = i * (oldSize - 1) / newSize;
auto const mod = i * (oldSize - 1) % newSize;
if (mod == 0)
result[i] = v[idx];
else {
float const part = float(mod) / float(newSize);
result[i] = v[idx] * (1.0 - part) + v[idx + 1] * part;
}
}
return result;
}
static Path createArrayPath(HeapArray points, DrawType style, StackArray scale, float width, float height)
{
bool invert = false;
if (scale[0] >= scale[1]) {
invert = true;
std::swap(scale[0], scale[1]);
}
// More than a point per pixel will cause insane loads, and isn't actually helpful
// Instead, linearly interpolate the vector to a max size of width in pixels
if (points.size() > width) {
points = rescale(points, width);
}
// Need at least 4 points to draw a bezier curve
if (points.size() <= 4 && style == Curve)
style = Polygon;
// Add repeat of last point for Points style
if (style == Points)
points.add(points.back());
float const dh = height / (scale[1] - scale[0]);
float const dw = width / static_cast(points.size() - 1);
float const invh = invert ? 0 : height;
float const yscale = invert ? -1.0f : 1.0f;
// Convert y values to xy coordinates
HeapArray xyPoints;
xyPoints.reserve(points.size() * 2);
for (int x = 0; x < points.size(); x++) {
xyPoints.add(x * dw);
xyPoints.add(invh - (std::clamp(points[x], scale[0], scale[1]) - scale[0]) * dh * yscale);
}
auto const* pointPtr = xyPoints.data();
auto numPoints = xyPoints.size() / 2;
StackArray control;
control[4] = pointPtr[0];
control[5] = pointPtr[1];
pointPtr += 2;
Path result;
if (Point(control[4], control[5]).isFinite()) {
result.startNewSubPath(control[4], control[5]);
}
for (int i = numPoints - 2; i > 0; i--, pointPtr += 2) {
switch (style) {
case Points: {
if (Point(pointPtr[0], control[5]).isFinite() && Point(pointPtr[0], pointPtr[1]).isFinite()) {
result.lineTo(pointPtr[0], control[5]);
result.startNewSubPath(pointPtr[0], pointPtr[1]);
}
if (i == 1 && Point(pointPtr[2], pointPtr[3]).isFinite()) {
result.lineTo(pointPtr[2], pointPtr[3]);
}
control[4] = pointPtr[0];
control[5] = pointPtr[1];
break;
}
case Polygon: {
if (Point(pointPtr[0], pointPtr[1]).isFinite()) {
result.lineTo(pointPtr[0], pointPtr[1]);
}
if (i == 1 && Point(pointPtr[2], pointPtr[3]).isFinite()) {
result.lineTo(pointPtr[2], pointPtr[3]);
}
break;
}
case Curve: {
// Curve logic taken from tcl/tk source code:
// https://github.com/tcltk/tk/blob/c9fe293db7a52a34954db92d2bdc5454d4de3897/generic/tkTrig.c#L1363
control[0] = 0.333 * control[4] + 0.667 * pointPtr[0];
control[1] = 0.333 * control[5] + 0.667 * pointPtr[1];
// Set up the last two control points. This is done differently for
// the last spline of an open curve than for other cases.
if (i == 1) {
control[4] = pointPtr[2];
control[5] = pointPtr[3];
} else {
control[4] = 0.5 * pointPtr[0] + 0.5 * pointPtr[2];
control[5] = 0.5 * pointPtr[1] + 0.5 * pointPtr[3];
}
control[2] = 0.333 * control[4] + 0.667 * pointPtr[0];
control[3] = 0.333 * control[5] + 0.667 * pointPtr[1];
auto start = Point(control[0], control[1]);
auto c1 = Point(control[2], control[3]);
auto end = Point(control[4], control[5]);
if (start.isFinite() && c1.isFinite() && end.isFinite()) {
result.cubicTo(start, c1, end);
}
break;
}
}
}
return result;
}
void paintGraph(Graphics& g)
{
auto const h = static_cast(getHeight());
auto const w = static_cast(getWidth());
if (vec.not_empty()) {
auto p = createArrayPath(vec, getDrawType(), getScale(), w, h);
g.setColour(getContentColour());
g.strokePath(p, PathStrokeType(getLineWidth()));
}
}
void paintGraph(NVGcontext* nvg)
{
NVGScopedState scopedState(nvg);
auto const h = static_cast(getHeight());
auto const w = static_cast(getWidth());
auto const arrB = Rectangle(0, 0, w, h).reduced(1);
nvgIntersectRoundedScissor(nvg, arrB.getX(), arrB.getY(), arrB.getWidth(), arrB.getHeight(), Corners::objectCornerRadius);
if (vec.not_empty()) {
auto p = createArrayPath(vec, getDrawType(), getScale(), w, h);
setJUCEPath(nvg, p);
auto contentColour = getContentColour();
nvgStrokeColor(nvg, nvgRGBA(contentColour.getRed(), contentColour.getGreen(), contentColour.getBlue(), contentColour.getAlpha()));
nvgStrokeWidth(nvg, getLineWidth());
nvgStroke(nvg);
}
}
void receiveMessage(t_symbol* symbol, SmallArray<:atom> const& atoms) override
{
switch (hash(symbol->s_name)) {
case hash("edit"): {
if (atoms.size() <= 0)
break;
MessageManager::callAsync([_this = SafePointer(this), shouldBeEditable = static_cast(atoms[0].getFloat())]() {
if (_this) {
_this->editable = shouldBeEditable;
_this->setInterceptsMouseClicks(shouldBeEditable, false);
}
});
break;
}
case hash("rename"): {
if (atoms.size() <= 0)
break;
MessageManager::callAsync([_this = SafePointer(this), newName = atoms[0].toString()]() {
if (_this) {
_this->object->cnv->setSelected(_this->object, false);
_this->object->editor->sidebar->hideParameters();
_this->name = newName;
}
});
break;
}
case hash("color"): {
MessageManager::callAsync([_this = SafePointer(this)] {
if (_this)
_this->repaint();
});
break;
}
case hash("width"): {
MessageManager::callAsync([_this = SafePointer(this)] {
if (_this)
_this->repaint();
});
break;
}
case hash("style"): {
MessageManager::callAsync([_this = SafePointer(this), newDrawMode = static_cast(atoms[0].getFloat())] {
if (_this) {
_this->drawMode = newDrawMode + 1;
_this->updateSettings();
_this->repaint();
}
});
break;
}
case hash("xticks"): {
MessageManager::callAsync([_this = SafePointer(this)] {
if (_this)
_this->repaint();
});
break;
}
case hash("yticks"): {
MessageManager::callAsync([_this = SafePointer(this)] {
if (_this)
_this->repaint();
});
break;
}
case hash("vis"): {
MessageManager::callAsync([_this = SafePointer(this), visible = atoms[0].getFloat()] {
if (_this) {
_this->visible = static_cast(visible);
_this->repaint();
}
});
break;
}
case hash("resize"): {
MessageManager::callAsync([_this = SafePointer(this), newSize = atoms[0].getFloat()] {
if (_this) {
_this->size = static_cast(newSize);
_this->updateSettings();
_this->repaint();
}
});
break;
}
}
}
void render(NVGcontext* nvg) override
{
if (error) {
auto position = getLocalBounds().getCentre();
auto errorText = "array " + getUnexpandedName() + " is invalid";
nvgFontSize(nvg, 11);
nvgFontFace(nvg, "Inter-Regular");
nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
nvgFillColor(nvg, convertColour(object->getLookAndFeel().findColour(PlugDataColour::canvasTextColourId)));
nvgText(nvg, position.x, position.y, errorText.toRawUTF8(), nullptr);
error = false;
} else if (visible) {
paintGraph(nvg);
}
}
void paint(Graphics& g) override
{
if (error) {
// TODO: error colour
Fonts::drawText(g, "array " + getUnexpandedName() + " is invalid", 0, 0, getWidth(), getHeight(), object->getLookAndFeel().findColour(PlugDataColour::canvasTextColourId), 15, Justification::centred);
error = false;
} else if (visible) {
paintGraph(g);
}
}
void mouseDown(MouseEvent const& e) override
{
if (error || !getEditMode())
return;
edited = true;
auto const s = static_cast(vec.size() - 1);
auto const w = static_cast(getWidth());
auto const x = static_cast(e.x);
lastIndex = std::round(std::clamp(x / w, 0.f, 1.f) * s);
mouseDrag(e);
}
void mouseDrag(MouseEvent const& e) override
{
if (error || !getEditMode())
return;
auto const s = static_cast(vec.size() - 1);
auto const w = static_cast(getWidth());
auto const h = static_cast(getHeight());
auto const x = static_cast(e.x);
auto const y = static_cast(e.y);
StackArray scale = getScale();
int const index = static_cast(std::round(std::clamp(x / w, 0.f, 1.f) * s));
float start = vec[lastIndex];
float current = (1.f - std::clamp(y / h, 0.f, 1.f)) * (scale[1] - scale[0]) + scale[0];
int interpStart = std::min(index, lastIndex);
int interpEnd = std::max(index, lastIndex);
float min = index == interpStart ? current : start;
float max = index == interpStart ? start : current;
// Fix to make sure we don't leave any gaps while dragging
for (int n = interpStart; n <= interpEnd; n++) {
vec[n] = jmap(n, interpStart, interpEnd + 1, min, max);
}
// Don't want to touch vec on the other thread, so we copy the vector into the lambda
auto changed = HeapArray(vec.begin() + interpStart, vec.begin() + interpEnd + 1);
lastIndex = index;
if (auto ptr = arr.get()) {
for (int n = 0; n < changed.size(); n++) {
write(ptr.get(), interpStart + n, changed[n]);
}
pd->sendDirectMessage(ptr.get(), "array");
}
repaint();
}
void mouseUp(MouseEvent const& e) override
{
if (error || !getEditMode())
return;
if (auto ptr = arr.get()) {
plugdata_forward_message(ptr->x_glist, gensym("redraw"), 0, NULL);
}
edited = false;
}
void update()
{
size = getArraySize();
if (!edited) {
bool changed = read(vec);
if (changed)
repaint();
}
}
bool willSaveContent() const
{
if (auto ptr = arr.get()) {
return ptr->x_saveit;
}
return false;
}
// Gets the text label of the array
String getUnexpandedName() const
{
if (auto ptr = arr.get()) {
return String::fromUTF8(ptr->x_name->s_name);
}
return {};
}
int getLineWidth()
{
if (auto ptr = arr.get()) {
if (ptr->x_scalar) {
t_scalar* scalar = ptr->x_scalar;
t_template* scalartplte = template_findbyname(scalar->sc_template);
if (scalartplte) {
int result = (int)template_getfloat(scalartplte, gensym("linewidth"), scalar->sc_vec, 1);
return result;
}
}
}
return 1;
}
DrawType getDrawType() const
{
if (auto ptr = arr.get()) {
if (ptr->x_scalar) {
t_scalar* scalar = ptr->x_scalar;
t_template* scalartplte = template_findbyname(scalar->sc_template);
if (scalartplte) {
int result = (int)template_getfloat(scalartplte, gensym("style"), scalar->sc_vec, 0);
return static_cast(result);
}
}
}
return DrawType::Points;
}
// Gets the scale of the array
StackArray getScale() const
{
if (auto ptr = arr.get()) {
t_canvas const* cnv = ptr->x_glist;
if (cnv) {
float min = cnv->gl_y2;
float max = cnv->gl_y1;
if (approximatelyEqual(min, max))
max += 1e-6;
return { min, max };
}
}
return { -1.0f, 1.0f };
}
bool getEditMode() const
{
if (auto ptr = arr.get()) {
return ptr->x_edit;
}
return true;
}
// Gets the scale of the array.
int getArraySize() const
{
if (auto ptr = arr.get()) {
return garray_getarray(ptr.get())->a_n;
}
return 0;
}
Colour getContentColour()
{
if (auto garray = arr.get()) {
auto* scalar = garray->x_scalar;
auto* templ = template_findbyname(scalar->sc_template);
int colour = template_getfloat(templ, gensym("color"), scalar->sc_vec, 1);
if (colour <= 0) {
return object->cnv->editor->getLookAndFeel().findColour(PlugDataColour::canvasTextColourId);
}
auto rangecolor = [](int n) /* 0 to 9 in 5 steps */
{
int n2 = (n == 9 ? 8 : n); /* 0 to 8 */
int ret = (n2 << 5); /* 0 to 256 in 9 steps */
if (ret > 255)
ret = 255;
return (ret);
};
int red = rangecolor(colour / 100);
int green = rangecolor((colour / 10) % 10);
int blue = rangecolor(colour % 10);
return Colour(red, green, blue);
}
return object->cnv->editor->getLookAndFeel().findColour(PlugDataColour::guiObjectInternalOutlineColour);
}
void valueChanged(Value& value) override
{
if (value.refersToSameSourceAs(name) || value.refersToSameSourceAs(size) || value.refersToSameSourceAs(drawMode) || value.refersToSameSourceAs(saveContents)) {
updateSettings();
repaint();
} else if (value.refersToSameSourceAs(range)) {
auto min = static_cast(range.getValue().getArray()->getReference(0));
auto max = static_cast(range.getValue().getArray()->getReference(1));
setScale({ min, max });
repaint();
}
}
void updateSettings()
{
auto arrName = name.getValue().toString();
auto arrSize = std::max(0, getValue(size));
auto arrDrawMode = getValue(drawMode) - 1;
if (arrSize != getValue(size)) {
size = arrSize;
}
// This flag is swapped for some reason
if (arrDrawMode == 0) {
arrDrawMode = 1;
} else if (arrDrawMode == 1) {
arrDrawMode = 0;
}
auto arrSaveContents = getValue(saveContents);
int flags = arrSaveContents + 2 * static_cast(arrDrawMode);
t_symbol* name = pd->generateSymbol(arrName);
if (auto garray = arr.get()) {
garray_arraydialog(garray.get(), name, arrSize, static_cast(flags), 0.0f);
}
object->gui->updateLabel();
}
void deleteArray()
{
if (auto garray = arr.get()) {
glist_delete(garray->x_glist, &garray->x_gobj);
}
reloadGraphs();
}
void updateParameters()
{
auto scale = getScale();
range = var(VarArray { var(scale[0]), var(scale[1]) });
size = var(static_cast(getArraySize()));
saveContents = willSaveContent();
name = String(getUnexpandedName());
drawMode = static_cast(getDrawType()) + 1;
repaint();
}
void setScale(StackArray scale)
{
auto& [min, max] = scale.data_;
if (auto ptr = arr.get()) {
t_canvas* cnv = ptr->x_glist;
if (cnv) {
cnv->gl_y2 = min;
cnv->gl_y1 = max;
return;
}
}
}
// Gets the values from the array.
bool read(HeapArray& output) const
{
bool changed = false;
if (auto ptr = arr.get()) {
int const size = garray_getarray(ptr.get())->a_n;
output.resize(static_cast(size));
t_word* vec = ((t_word*)garray_vec(ptr.get()));
for (int i = 0; i < size; i++) {
changed = changed || output[i] != vec[i].w_float;
output[i] = vec[i].w_float;
}
}
return changed;
}
// Writes a value to the array.
void write(t_garray* garray, size_t const pos, float const input)
{
if(pos < garray_npoints(garray)) {
t_word* vec = ((t_word*)garray_vec(garray));
vec[pos].w_float = input;
}
}
pd::WeakReference arr;
HeapArray vec;
AtomicValue edited;
bool error = false;
int lastIndex = 0;
PluginProcessor* pd;
bool editable = true;
};
struct ArrayPropertiesPanel : public PropertiesPanelProperty
, public Value::Listener {
class AddArrayButton : public Component {
bool mouseIsOver = false;
public:
std::function onClick = []() { };
void paint(Graphics& g) override
{
auto bounds = getLocalBounds();
auto textBounds = bounds;
auto iconBounds = textBounds.removeFromLeft(textBounds.getHeight());
auto colour = findColour(PlugDataColour::sidebarTextColourId);
if (mouseIsOver) {
g.setColour(findColour(PlugDataColour::sidebarActiveBackgroundColourId));
g.fillRoundedRectangle(bounds.toFloat(), Corners::defaultCornerRadius);
}
Fonts::drawIcon(g, Icons::Add, iconBounds, colour, 12);
Fonts::drawText(g, "Add new array", textBounds, colour, 14);
}
bool hitTest(int x, int y) override
{
if (getLocalBounds().reduced(5, 2).contains(x, y)) {
return true;
}
return false;
}
void mouseEnter(MouseEvent const& e) override
{
mouseIsOver = true;
repaint();
}
void mouseExit(MouseEvent const& e) override
{
mouseIsOver = false;
repaint();
}
void mouseUp(MouseEvent const& e) override
{
onClick();
}
};
OwnedArray properties;
SmallArray> graphs;
AddArrayButton addButton;
OwnedArray deleteButtons;
HeapArray nameValues;
std::function syncCanvas = []() { };
ArrayPropertiesPanel(std::function addArrayCallback, std::function syncCanvasFunc)
: PropertiesPanelProperty("array")
{
setHideLabel(true);
addAndMakeVisible(addButton);
addButton.onClick = addArrayCallback;
syncCanvas = syncCanvasFunc;
}
void reloadGraphs(SmallArray> const& safeGraphs)
{
properties.clear();
nameValues.clear();
deleteButtons.clear();
graphs = safeGraphs;
nameValues.reserve(graphs.size());
for (auto graph : graphs) {
addAndMakeVisible(properties.add(new PropertiesPanel::EditableComponent("Name", graph->name)));
addAndMakeVisible(properties.add(new PropertiesPanel::EditableComponent("Size", graph->size)));
addAndMakeVisible(properties.add(new PropertiesPanel::RangeComponent("Range", graph->range, false)));
addAndMakeVisible(properties.add(new PropertiesPanel::BoolComponent("Save contents", graph->saveContents, { "No", "Yes" })));
addAndMakeVisible(properties.add(new PropertiesPanel::ComboComponent("Draw Style", graph->drawMode, { "Points", "Polygon", "Bezier curve" })));
// To detect name changes, so we can redraw the array title
nameValues.add(Value());
auto& nameValue = nameValues[nameValues.size() - 1];
nameValue.referTo(graph->name);
nameValue.addListener(this);
auto* deleteButton = deleteButtons.add(new SmallIconButton(Icons::Clear));
deleteButton->onClick = [graph]() {
graph->deleteArray();
};
addAndMakeVisible(deleteButton);
}
auto newHeight = (156 * graphs.size()) + 34;
setPreferredHeight(newHeight);
if (auto* propertiesPanel = findParentComponentOfClass()) {
propertiesPanel->updatePropHolderLayout();
}
repaint();
}
void valueChanged(Value& v) override
{
// when array parameters are changed we need to resync the canavs to PD
// TODO: do we need to protect this in a callasync also?
syncCanvas();
repaint();
}
void paint(Graphics& g) override
{
g.fillAll(findColour(PlugDataColour::sidebarBackgroundColourId));
auto numGraphs = properties.size() / 5;
for (int i = 0; i < numGraphs; i++) {
if (!graphs[i])
continue;
auto start = (i * 156) - 6;
g.setColour(findColour(PlugDataColour::sidebarActiveBackgroundColourId));
g.fillRoundedRectangle(0.0f, start + 25, getWidth(), 130, Corners::largeCornerRadius);
Fonts::drawStyledText(g, graphs[i]->name.toString(), 8, start - 2, getWidth() - 16, 25, findColour(PlugDataColour::sidebarTextColourId), Semibold, 14.5f);
}
g.setColour(findColour(PlugDataColour::sidebarBackgroundColourId));
for (int i = 0; i < properties.size(); i++) {
if ((i % 5) == 4)
continue;
auto y = properties[i]->getBottom();
g.drawHorizontalLine(y, 0, getWidth());
}
}
void resized() override
{
auto b = getLocalBounds().translated(0, -6);
for (int i = 0; i < properties.size(); i++) {
if ((i % 5) == 0) {
auto deleteButtonBounds = b.removeFromTop(26).removeFromRight(28);
deleteButtons[i / 5]->setBounds(deleteButtonBounds);
}
properties[i]->setBounds(b.removeFromTop(26));
}
addButton.setBounds(getLocalBounds().removeFromBottom(36).reduced(0, 8));
}
};
class ArrayListView : public PropertiesPanel
, public Value::Listener {
public:
ArrayListView(pd::Instance* instance, void* arr)
: array(arr, instance)
{
update();
}
void parentSizeChanged() override
{
setContentWidth(getWidth() - 100);
}
void update()
{
clear();
arrayValues.clear();
PropertiesArray properties;
if (auto ptr = array.get()) {
auto* arr = garray_getarray(ptr.cast());
auto* vec = ((t_word*)garray_vec(ptr.cast()));
auto numProperties = arr->a_n;
properties.resize(numProperties);
for (int i = 0; i < numProperties; i++) {
auto& value = *arrayValues.add(new Value(vec[i].w_float));
value.addListener(this);
auto* property = new EditableComponent(String(i), value);
auto* label = dynamic_cast(property->label.get());
property->setRangeMin(ptr->x_glist->gl_y2);
property->setRangeMax(ptr->x_glist->gl_y1);
// Only send this after drag end so it doesn't interrupt the drag action
label->dragEnd = [this]() {
if (auto ptr = array.get()) {
plugdata_forward_message(ptr->x_glist, gensym("redraw"), 0, NULL);
}
};
properties.set(i, property);
}
}
addSection("", properties);
}
private:
void valueChanged(Value& v) override
{
if (auto ptr = array.get()) {
auto* vec = ((t_word*)garray_vec(ptr.cast()));
for (int i = 0; i < arrayValues.size(); i++) {
auto& value = *arrayValues[i];
if (v.refersToSameSourceAs(value)) {
vec[i].w_float = getValue(value);
break;
}
}
}
}
OwnedArray arrayValues;
pd::WeakReference array;
};
class ArrayEditorDialog : public Component {
ResizableBorderComponent resizer;
std::unique_ptr