diff --git a/Makefile b/Makefile index 0555600f..70ffcf23 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ include compile.mk dist: all ifndef VERSION - $(error VERSION must be defined when calling make) + $(error VERSION must be defined when making distributables) endif rm -rf dist $(MAKE) -C plugins/Fundamental dist diff --git a/README.md b/README.md index eb7f4a0d..7c6e567c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This README includes instructions for building Rack from source. For information ## The [Issue Tracker](https://github.com/VCVRack/Rack/issues) *is* the official developer's forum Bug reports, feature requests, and even *questions/discussions* are welcome on the GitHub Issue Tracker for all VCVRack repos. -However, please search before posting to avoid duplicates. +However, please search before posting to avoid duplicates, and limit to one issue per post. You may vote on feature requests by using the Thumbs Up/Down reaction on the first post. diff --git a/compile.mk b/compile.mk index cf0e6bf4..0ebc1501 100644 --- a/compile.mk +++ b/compile.mk @@ -42,7 +42,6 @@ DEPS = $(patsubst %, build/%.d, $(SOURCES)) $(TARGET): $(OBJECTS) $(CXX) -o $@ $^ $(LDFLAGS) -# Object targets -include $(DEPS) diff --git a/include/asset.hpp b/include/asset.hpp index 53f94458..dc5957a9 100644 --- a/include/asset.hpp +++ b/include/asset.hpp @@ -7,13 +7,13 @@ namespace rack { -/** Searches for a global read-only resource and returns its path, or "" if not found +/** Returns the path of a global resource. Read-only */ std::string assetGlobal(std::string filename); -/** Searches for a local resource +/** Returns the path of a local resource. Read/write */ std::string assetLocal(std::string filename); -/** Searches for a plugin resource, given a Plugin object +/** Returns the path of a resource in the plugin's folder. Read-only */ std::string assetPlugin(Plugin *plugin, std::string filename); diff --git a/include/plugin.hpp b/include/plugin.hpp index 19b7693e..01b51242 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -11,32 +11,37 @@ struct Model; // Subclass this and return a pointer to a new one when init() is called struct Plugin { - virtual ~Plugin(); - - /** A unique identifier for your plugin, e.g. "foo" */ - std::string slug; - /** Human readable name for your plugin, e.g. "Foo Modular" */ - std::string name; - /** A list of the models made available by this plugin */ + /** A list of the models available by this plugin, add with addModel() */ std::list models; - /** The file path of the plugins directory */ + /** The file path of the plugin's directory */ std::string path; /** OS-dependent library handle */ void *handle = NULL; - /** Optional metadata for the Add Module context menu */ - std::string homepageUrl; - std::string manualUrl; + + /** Used when syncing plugins with the API */ + std::string slug; + /** The version of your plugin + Plugins should follow the versioning scheme described at https://github.com/VCVRack/Rack/issues/266 + Do not include the "v" in "v1.0" for example. + */ std::string version; + + virtual ~Plugin(); + void addModel(Model *model); }; struct Model { - virtual ~Model() {} - - Plugin *plugin; - /** A unique identifier for the model in this plugin, e.g. "VCO" */ + Plugin *plugin = NULL; + /** An identifier for the model, e.g. "VCO". Used for saving patches. The slug, manufacturerSlug pair must be unique. */ std::string slug; /** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ std::string name; + /** An identifier for the manufacturer, e.g. "foo". Used for saving patches. */ + std::string manufacturerSlug; + /** Human readable name for the manufacturer, e.g. "Foo Modular" */ + std::string manufacturerName; + + virtual ~Model() {} virtual ModuleWidget *createModuleWidget() { return NULL; } }; @@ -63,6 +68,7 @@ std::string pluginGetLoginStatus(); //////////////////// /** Called once to initialize and return the Plugin instance. +You must implement this in your plugin */ extern "C" void init(rack::Plugin *plugin); diff --git a/include/rack.hpp b/include/rack.hpp index abf27412..93ebaf53 100644 --- a/include/rack.hpp +++ b/include/rack.hpp @@ -18,7 +18,7 @@ namespace rack { //////////////////// template -Model *createModel(Plugin *plugin, std::string slug, std::string name) { +Model *createModel(std::string manufacturerSlug, std::string manufacturerName, std::string slug, std::string name) { struct TModel : Model { ModuleWidget *createModuleWidget() override { ModuleWidget *moduleWidget = new TModuleWidget(); @@ -27,13 +27,10 @@ Model *createModel(Plugin *plugin, std::string slug, std::string name) { } }; Model *model = new TModel(); - model->plugin = plugin; model->slug = slug; model->name = name; - // Create bi-directional association between the Plugin and Model - if (plugin) { - plugin->models.push_back(model); - } + model->manufacturerSlug = manufacturerSlug; + model->manufacturerName = manufacturerName; return model; } diff --git a/include/widgets.hpp b/include/widgets.hpp index 16795064..af9b1346 100644 --- a/include/widgets.hpp +++ b/include/widgets.hpp @@ -399,6 +399,7 @@ struct ZoomWidget : Widget { struct TextField : OpaqueWidget { std::string text; std::string placeholder; + bool multiline = false; int begin = 0; int end = 0; diff --git a/plugin.mk b/plugin.mk index b40c4b9a..1977a1d0 100644 --- a/plugin.mk +++ b/plugin.mk @@ -24,7 +24,7 @@ endif all: $(TARGET) +include ../../compile.mk + clean: rm -rfv build $(TARGET) dist - -include ../../compile.mk diff --git a/src/app/ModuleWidget.cpp b/src/app/ModuleWidget.cpp index 1d768575..493df945 100644 --- a/src/app/ModuleWidget.cpp +++ b/src/app/ModuleWidget.cpp @@ -45,8 +45,8 @@ void ModuleWidget::addParam(ParamWidget *param) { json_t *ModuleWidget::toJson() { json_t *rootJ = json_object(); - // plugin - json_object_set_new(rootJ, "plugin", json_string(model->plugin->slug.c_str())); + // manufacturer + json_object_set_new(rootJ, "manufacturer", json_string(model->manufacturerSlug.c_str())); // model json_object_set_new(rootJ, "model", json_string(model->slug.c_str())); // pos @@ -245,7 +245,7 @@ Menu *ModuleWidget::createContextMenu() { Menu *menu = gScene->createMenu(); MenuLabel *menuLabel = new MenuLabel(); - menuLabel->text = model->plugin->name + ": " + model->name; + menuLabel->text = model->manufacturerName + " " + model->name; menu->pushChild(menuLabel); ResetMenuItem *resetItem = new ResetMenuItem(); diff --git a/src/app/RackScene.cpp b/src/app/RackScene.cpp index a4c8bd81..98b8e52d 100644 --- a/src/app/RackScene.cpp +++ b/src/app/RackScene.cpp @@ -2,6 +2,7 @@ #include "gui.hpp" #include "util/request.hpp" #include "../ext/osdialog/osdialog.h" +#include #include @@ -17,7 +18,7 @@ static void checkVersion() { json_t *versionJ = json_object_get(resJ, "version"); if (versionJ) { const char *version = json_string_value(versionJ); - if (version && version != gApplicationVersion) { + if (version && strlen(version) > 0 && version != gApplicationVersion) { newVersion = version; } } @@ -45,7 +46,7 @@ RackScene::RackScene() { scrollWidget->box.pos.y = gToolbar->box.size.y; // Check for new version - if (gApplicationVersion != "dev") { + if (!gApplicationVersion.empty()) { std::thread versionThread(checkVersion); versionThread.detach(); } diff --git a/src/app/RackWidget.cpp b/src/app/RackWidget.cpp index 87de4129..a318ffc3 100644 --- a/src/app/RackWidget.cpp +++ b/src/app/RackWidget.cpp @@ -1,12 +1,13 @@ -#include -#include -#include #include "app.hpp" #include "engine.hpp" #include "plugin.hpp" #include "gui.hpp" #include "settings.hpp" #include "asset.hpp" +#include +#include +#include +#include #include "../ext/osdialog/osdialog.h" @@ -200,36 +201,29 @@ void RackWidget::fromJson(json_t *rootJ) { size_t moduleId; json_t *moduleJ; json_array_foreach(modulesJ, moduleId, moduleJ) { - json_t *pluginSlugJ = json_object_get(moduleJ, "plugin"); - if (!pluginSlugJ) continue; + json_t *manufacturerSlugJ = json_object_get(moduleJ, "manufacturer"); + if (!manufacturerSlugJ) { + // Backward compatibility with Rack v0.4 and lower + manufacturerSlugJ = json_object_get(moduleJ, "plugin"); + if (!manufacturerSlugJ) continue; + } json_t *modelSlugJ = json_object_get(moduleJ, "model"); if (!modelSlugJ) continue; - const char *pluginSlug = json_string_value(pluginSlugJ); - const char *modelSlug = json_string_value(modelSlugJ); - - // Search for plugin - Plugin *plugin = NULL; - for (Plugin *p : gPlugins) { - if (p->slug == pluginSlug) { - plugin = p; - break; - } - } - if (!plugin) { - message += stringf("Could not find plugin \"%s\" for module \"%s\".\n", pluginSlug, modelSlug); - continue; - } + std::string manufacturerSlug = json_string_value(manufacturerSlugJ); + std::string modelSlug = json_string_value(modelSlugJ); - // Get for model + // Search for model Model *model = NULL; - for (Model *m : plugin->models) { - if (m->slug == modelSlug) { - model = m; - break; + for (Plugin *plugin : gPlugins) { + for (Model *m : plugin->models) { + if (m->manufacturerSlug == manufacturerSlug && m->slug == modelSlug) { + model = m; + } } } + if (!model) { - message += stringf("Could not find module \"%s\" in plugin \"%s\".\n", modelSlug, pluginSlug); + message += stringf("Could not find \"%s %s\" module\n", manufacturerSlug.c_str(), modelSlug.c_str()); continue; } @@ -388,13 +382,23 @@ struct UrlItem : MenuItem { } }; -struct AddPluginMenuItem : MenuItem { - Plugin *plugin; +struct AddManufacturerMenuItem : MenuItem { + std::string manufacturerName; Vec modulePos; Menu *createChildMenu() override { + // Collect models which have this manufacturer name + std::set models; + for (Plugin *plugin : gPlugins) { + for (Model *model : plugin->models) { + if (model->manufacturerName == manufacturerName) { + models.insert(model); + } + } + } + // Model items Menu *menu = new Menu(); - for (Model *model : plugin->models) { + for (Model *model : models) { AddModuleMenuItem *item = new AddModuleMenuItem(); item->text = model->name; item->model = model; @@ -403,6 +407,7 @@ struct AddPluginMenuItem : MenuItem { } // Metadata items + /* { MenuLabel *label = new MenuLabel(); menu->pushChild(label); @@ -439,6 +444,7 @@ struct AddPluginMenuItem : MenuItem { item->text = "Version: v" + plugin->version; menu->pushChild(item); } + */ return menu; } @@ -452,10 +458,18 @@ void RackWidget::onMouseDownOpaque(int button) { MenuLabel *menuLabel = new MenuLabel(); menuLabel->text = "Add module"; menu->pushChild(menuLabel); + // Collect manufacturer names + std::set manufacturerNames; for (Plugin *plugin : gPlugins) { - AddPluginMenuItem *item = new AddPluginMenuItem(); - item->text = plugin->name; - item->plugin = plugin; + for (Model *model : plugin->models) { + manufacturerNames.insert(model->manufacturerName); + } + } + // Add menu item for each manufacturer name + for (std::string manufacturerName : manufacturerNames) { + AddManufacturerMenuItem *item = new AddManufacturerMenuItem(); + item->text = manufacturerName; + item->manufacturerName = manufacturerName; item->modulePos = modulePos; menu->pushChild(item); } diff --git a/src/core/MidiClockToCV.cpp b/src/core/MidiClockToCV.cpp index cd94991f..d37ef9ff 100644 --- a/src/core/MidiClockToCV.cpp +++ b/src/core/MidiClockToCV.cpp @@ -241,7 +241,7 @@ MIDIClockToCVWidget::MIDIClockToCVWidget() { { Label *label = new Label(); label->box.pos = Vec(box.size.x - margin - 7 * 15, margin); - label->text = "MIDI Clock to CV"; + label->text = "MIDI Clk-CV"; addChild(label); yPos = labelHeight * 2; } diff --git a/src/core/Notes.cpp b/src/core/Notes.cpp new file mode 100644 index 00000000..3395176f --- /dev/null +++ b/src/core/Notes.cpp @@ -0,0 +1,44 @@ +#include "core.hpp" + +using namespace rack; + + + +NotesWidget::NotesWidget() { + box.size = Vec(RACK_GRID_WIDTH * 18, RACK_GRID_HEIGHT); + + { + Panel *panel = new LightPanel(); + panel->box.size = box.size; + addChild(panel); + } + + addChild(createScrew(Vec(15, 0))); + addChild(createScrew(Vec(15, 365))); + addChild(createScrew(Vec(box.size.x - 30, 0))); + addChild(createScrew(Vec(box.size.x - 30, 365))); + + textField = new TextField(); + textField->box.pos = Vec(15, 15); + textField->box.size = box.size.minus(Vec(30, 30)); + textField->multiline = true; + addChild(textField); +} + +json_t *NotesWidget::toJson() { + json_t *rootJ = ModuleWidget::toJson(); + + // text + json_object_set_new(rootJ, "text", json_string(textField->text.c_str())); + + return rootJ; +} + +void NotesWidget::fromJson(json_t *rootJ) { + ModuleWidget::fromJson(rootJ); + + // text + json_t *textJ = json_object_get(rootJ, "text"); + if (textJ) + textField->text = json_string_value(textJ); +} diff --git a/src/core/core.cpp b/src/core/core.cpp index a083c008..a8192064 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -2,15 +2,14 @@ #include "MidiIO.hpp" -void init(rack::Plugin *plugin) { - plugin->slug = "Core"; - plugin->name = "Core"; - plugin->homepageUrl = "https://vcvrack.com/"; - createModel(plugin, "AudioInterface", "Audio Interface"); - createModel(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface"); - createModel(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface"); - createModel(plugin, "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface"); - createModel(plugin, "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface"); - // createModel(plugin, "Bridge", "Bridge"); - createModel(plugin, "Blank", "Blank"); +void init(rack::Plugin *p) { + p->slug = "Core"; + p->addModel(createModel("Core", "Core", "AudioInterface", "Audio Interface")); + p->addModel(createModel("Core", "Core", "MIDIToCVInterface", "MIDI-to-CV Interface")); + p->addModel(createModel("Core", "Core", "MIDICCToCVInterface", "MIDI CC-to-CV Interface")); + p->addModel(createModel("Core", "Core", "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface")); + p->addModel(createModel("Core", "Core", "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface")); + // p->addModel(createModel("Core", "Core", "Bridge", "Bridge")); + p->addModel(createModel("Core", "Core", "Blank", "Blank")); + p->addModel(createModel("Core", "Core", "Notes", "Notes")); } diff --git a/src/core/core.hpp b/src/core/core.hpp index 661cd87b..c5df6b79 100644 --- a/src/core/core.hpp +++ b/src/core/core.hpp @@ -26,3 +26,10 @@ struct BlankWidget : ModuleWidget { json_t *toJson() override; void fromJson(json_t *rootJ) override; }; + +struct NotesWidget : ModuleWidget { + TextField *textField; + NotesWidget(); + json_t *toJson() override; + void fromJson(json_t *rootJ) override; +}; diff --git a/src/plugin.cpp b/src/plugin.cpp index 3de1457b..75f4519b 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -44,6 +44,13 @@ Plugin::~Plugin() { } } +void Plugin::addModel(Model *model) { + assert(!model->plugin); + model->plugin = this; + models.push_back(model); +} + + static int loadPlugin(std::string path) { std::string libraryFilename; #if ARCH_LIN @@ -70,7 +77,7 @@ static int loadPlugin(std::string path) { } #endif - // Call plugin init() function + // Call plugin's init() function typedef void (*InitCallback)(Plugin *); InitCallback initCallback; #if ARCH_WIN @@ -89,21 +96,12 @@ static int loadPlugin(std::string path) { plugin->handle = handle; initCallback(plugin); - // Check that this is a unique slug - for (Plugin *otherPlugin : gPlugins) { - assert(plugin->slug != otherPlugin->slug); - } - // Add plugin to list gPlugins.push_back(plugin); fprintf(stderr, "Loaded plugin %s\n", libraryFilename.c_str()); return 0; } -static bool comparePlugins(Plugin *a, Plugin *b) { - return a->slug < b->slug; -} - static void loadPlugins(std::string path) { DIR *dir = opendir(path.c_str()); if (dir) { @@ -115,9 +113,6 @@ static void loadPlugins(std::string path) { } closedir(dir); } - - // Sort plugins - gPlugins.sort(comparePlugins); } //////////////////// @@ -200,13 +195,6 @@ static void refreshPurchase(json_t *pluginJ) { url += "&token="; url += gToken; - // Find slug in plugins list, and return silently if slug already exists - for (Plugin *p : gPlugins) { - if (p->slug == slug) { - return; - } - } - // If plugin is not loaded, download the zip file to /plugins downloadName = name; downloadProgress = 0.0; @@ -237,9 +225,9 @@ static void refreshPurchase(json_t *pluginJ) { void pluginInit() { // Load core // This function is defined in core.cpp - Plugin *corePlugin = new Plugin(); - init(corePlugin); - gPlugins.push_back(corePlugin); + Plugin *coreManufacturer = new Plugin(); + init(coreManufacturer); + gPlugins.push_back(coreManufacturer); // Load plugins from global directory std::string globalPlugins = assetGlobal("plugins"); @@ -248,10 +236,11 @@ void pluginInit() { // Load plugins from local directory std::string localPlugins = assetLocal("plugins"); - mkdir(localPlugins.c_str(), 0755); - printf("Loading plugins from %s\n", localPlugins.c_str()); - if (globalPlugins != localPlugins) + if (globalPlugins != localPlugins) { + mkdir(localPlugins.c_str(), 0755); + printf("Loading plugins from %s\n", localPlugins.c_str()); loadPlugins(localPlugins); + } } void pluginDestroy() { @@ -265,7 +254,8 @@ void pluginDestroy() { dlclose(plugin->handle); #endif - // For some reason this segfaults + // For some reason this segfaults. + // It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. // delete plugin; } gPlugins.clear(); diff --git a/src/widgets/FramebufferWidget.cpp b/src/widgets/FramebufferWidget.cpp index e34a4a11..21a14158 100644 --- a/src/widgets/FramebufferWidget.cpp +++ b/src/widgets/FramebufferWidget.cpp @@ -42,6 +42,7 @@ void FramebufferWidget::step() { internal->box.size = box.size; internal->box.size = Vec(ceilf(internal->box.size.x), ceilf(internal->box.size.y)); Vec fbSize = internal->box.size.mult(gPixelRatio * oversample); + // assert(fbSize.isFinite()); // Reject zero area size if (fbSize.x <= 0.0 || fbSize.y <= 0.0) @@ -71,6 +72,20 @@ void FramebufferWidget::step() { } void FramebufferWidget::draw(NVGcontext *vg) { + // { + // float xform[6]; + // nvgCurrentTransform(vg, xform); + // printf("%f %f %f %f %f %f\n", xform[0], xform[1], xform[2], xform[3], xform[4], xform[5]); + // nvgSave(vg); + // nvgResetTransform(vg); + // nvgTranslate(vg, xform[5], xform[6]); + // nvgBeginPath(vg); + // nvgRect(vg, 0, 0, 50, 50); + // nvgFillColor(vg, nvgRGBf(1.0, 0.0, 0.0)); + // nvgFill(vg); + // nvgRestore(vg); + // } + if (!internal->fb) { // Bypass framebuffer cache entirely // Widget::draw(vg); diff --git a/src/widgets/TextField.cpp b/src/widgets/TextField.cpp index 0e359b8e..16b051f4 100644 --- a/src/widgets/TextField.cpp +++ b/src/widgets/TextField.cpp @@ -101,6 +101,14 @@ bool TextField::onFocusKey(int key) { } } break; + case GLFW_KEY_ENTER: + if (multiline) { + insertText("\n"); + } + else { + onAction(); + } + break; } begin = mini(maxi(begin, 0), text.size());