See More

/* // 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