Notes to Core, changed plugin build system to support multiple targets per manufacturertags/v0.5.0
| @@ -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. | |||
| @@ -42,6 +42,15 @@ DEPS = $(patsubst %, build/%.d, $(SOURCES)) | |||
| $(TARGET): $(OBJECTS) | |||
| $(CXX) -o $@ $^ $(LDFLAGS) | |||
| %.so: | |||
| $(CXX) -o $@ $^ $(LDFLAGS) | |||
| %.dylib: | |||
| $(CXX) -o $@ $^ $(LDFLAGS) | |||
| %.dll: | |||
| $(CXX) -o $@ $^ $(LDFLAGS) | |||
| # Object targets | |||
| -include $(DEPS) | |||
| @@ -13,9 +13,9 @@ std::string assetGlobal(std::string filename); | |||
| /** Searches for a local resource | |||
| */ | |||
| std::string assetLocal(std::string filename); | |||
| /** Searches for a plugin resource, given a Plugin object | |||
| /** Searches for a manufacturer resource, given a Manufacturer object | |||
| */ | |||
| std::string assetPlugin(Plugin *plugin, std::string filename); | |||
| std::string assetManufacturer(Manufacturer *manufacturer, std::string filename); | |||
| } // namespace rack | |||
| @@ -10,37 +10,41 @@ struct ModuleWidget; | |||
| 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 */ | |||
| struct Manufacturer { | |||
| /** A list of the models available by this manufacturer, add with addModel() */ | |||
| std::list<Model*> models; | |||
| /** The file path of the plugins directory */ | |||
| std::string path; | |||
| /** OS-dependent library handle */ | |||
| void *handle = NULL; | |||
| // You may set everything below this point in your plugin | |||
| /** A unique identifier for the manufacturer, e.g. "foo" */ | |||
| std::string slug; | |||
| /** Human readable name for the manufacturer, e.g. "Foo Modular" */ | |||
| std::string name; | |||
| /** Optional metadata for the Add Module context menu */ | |||
| std::string homepageUrl; | |||
| std::string manualUrl; | |||
| std::string version; | |||
| virtual ~Manufacturer(); | |||
| void addModel(Model *model); | |||
| }; | |||
| struct Model { | |||
| virtual ~Model() {} | |||
| Plugin *plugin; | |||
| /** A unique identifier for the model in this plugin, e.g. "VCO" */ | |||
| Manufacturer *manufacturer = NULL; | |||
| /** A unique identifier for the model, e.g. "VCO" */ | |||
| std::string slug; | |||
| /** Human readable name for your model, e.g. "Voltage Controlled Oscillator" */ | |||
| std::string name; | |||
| virtual ~Model() {} | |||
| virtual ModuleWidget *createModuleWidget() { return NULL; } | |||
| }; | |||
| extern std::list<Plugin*> gPlugins; | |||
| extern std::list<Manufacturer*> gManufacturers; | |||
| extern std::string gToken; | |||
| void pluginInit(); | |||
| @@ -62,7 +66,8 @@ std::string pluginGetLoginStatus(); | |||
| // Implemented by plugin | |||
| //////////////////// | |||
| /** Called once to initialize and return the Plugin instance. | |||
| /** Called once to initialize and return the Manufacturer instance. | |||
| You must implement this in your plugin | |||
| */ | |||
| extern "C" | |||
| void init(rack::Plugin *plugin); | |||
| void init(rack::Manufacturer *manufacturer); | |||
| @@ -18,7 +18,7 @@ namespace rack { | |||
| //////////////////// | |||
| template <class TModuleWidget> | |||
| Model *createModel(Plugin *plugin, std::string slug, std::string name) { | |||
| Model *createModel(std::string slug, std::string name) { | |||
| struct TModel : Model { | |||
| ModuleWidget *createModuleWidget() override { | |||
| ModuleWidget *moduleWidget = new TModuleWidget(); | |||
| @@ -27,13 +27,8 @@ 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); | |||
| } | |||
| return model; | |||
| } | |||
| @@ -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; | |||
| @@ -8,23 +8,21 @@ include ../../arch.mk | |||
| ifeq ($(ARCH), lin) | |||
| LDFLAGS += -shared | |||
| TARGET = plugin.so | |||
| PLUGIN_EXTENSION = so | |||
| endif | |||
| ifeq ($(ARCH), mac) | |||
| LDFLAGS += -shared -undefined dynamic_lookup | |||
| TARGET = plugin.dylib | |||
| PLUGIN_EXTENSION = dylib | |||
| endif | |||
| ifeq ($(ARCH), win) | |||
| LDFLAGS += -shared -L../../ -lRack | |||
| TARGET = plugin.dll | |||
| PLUGIN_EXTENSION = dll | |||
| endif | |||
| all: $(TARGET) | |||
| clean: | |||
| rm -rfv build $(TARGET) dist | |||
| rm -rfv build *.$(PLUGIN_EXTENSION) dist | |||
| include ../../compile.mk | |||
| @@ -46,7 +46,7 @@ json_t *ModuleWidget::toJson() { | |||
| json_t *rootJ = json_object(); | |||
| // plugin | |||
| json_object_set_new(rootJ, "plugin", json_string(model->plugin->slug.c_str())); | |||
| json_object_set_new(rootJ, "plugin", json_string(model->manufacturer->slug.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->manufacturer->name + ": " + model->name; | |||
| menu->pushChild(menuLabel); | |||
| ResetMenuItem *resetItem = new ResetMenuItem(); | |||
| @@ -2,6 +2,7 @@ | |||
| #include "gui.hpp" | |||
| #include "util/request.hpp" | |||
| #include "../ext/osdialog/osdialog.h" | |||
| #include <string.h> | |||
| #include <thread> | |||
| @@ -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(); | |||
| } | |||
| @@ -208,21 +208,21 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
| const char *modelSlug = json_string_value(modelSlugJ); | |||
| // Search for plugin | |||
| Plugin *plugin = NULL; | |||
| for (Plugin *p : gPlugins) { | |||
| if (p->slug == pluginSlug) { | |||
| plugin = p; | |||
| Manufacturer *manufacturer = NULL; | |||
| for (Manufacturer *m : gManufacturers) { | |||
| if (m->slug == pluginSlug) { | |||
| manufacturer = m; | |||
| break; | |||
| } | |||
| } | |||
| if (!plugin) { | |||
| if (!manufacturer) { | |||
| message += stringf("Could not find plugin \"%s\" for module \"%s\".\n", pluginSlug, modelSlug); | |||
| continue; | |||
| } | |||
| // Get for model | |||
| Model *model = NULL; | |||
| for (Model *m : plugin->models) { | |||
| for (Model *m : manufacturer->models) { | |||
| if (m->slug == modelSlug) { | |||
| model = m; | |||
| break; | |||
| @@ -388,13 +388,13 @@ struct UrlItem : MenuItem { | |||
| } | |||
| }; | |||
| struct AddPluginMenuItem : MenuItem { | |||
| Plugin *plugin; | |||
| struct AddManufacturerMenuItem : MenuItem { | |||
| Manufacturer *manufacturer; | |||
| Vec modulePos; | |||
| Menu *createChildMenu() override { | |||
| // Model items | |||
| Menu *menu = new Menu(); | |||
| for (Model *model : plugin->models) { | |||
| for (Model *model : manufacturer->models) { | |||
| AddModuleMenuItem *item = new AddModuleMenuItem(); | |||
| item->text = model->name; | |||
| item->model = model; | |||
| @@ -409,34 +409,34 @@ struct AddPluginMenuItem : MenuItem { | |||
| } | |||
| { | |||
| MenuLabel *label = new MenuLabel(); | |||
| label->text = plugin->name; | |||
| label->text = manufacturer->name; | |||
| menu->pushChild(label); | |||
| } | |||
| if (!plugin->homepageUrl.empty()) { | |||
| if (!manufacturer->homepageUrl.empty()) { | |||
| UrlItem *item = new UrlItem(); | |||
| item->text = "Homepage"; | |||
| item->url = plugin->homepageUrl; | |||
| item->url = manufacturer->homepageUrl; | |||
| menu->pushChild(item); | |||
| } | |||
| if (!plugin->manualUrl.empty()) { | |||
| if (!manufacturer->manualUrl.empty()) { | |||
| UrlItem *item = new UrlItem(); | |||
| item->text = "Manual"; | |||
| item->url = plugin->manualUrl; | |||
| item->url = manufacturer->manualUrl; | |||
| menu->pushChild(item); | |||
| } | |||
| if (!plugin->path.empty()) { | |||
| if (!manufacturer->path.empty()) { | |||
| UrlItem *item = new UrlItem(); | |||
| item->text = "Browse directory"; | |||
| item->url = plugin->path; | |||
| item->url = manufacturer->path; | |||
| menu->pushChild(item); | |||
| } | |||
| if (!plugin->version.empty()) { | |||
| if (!manufacturer->version.empty()) { | |||
| MenuLabel *item = new MenuLabel(); | |||
| item->text = "Version: v" + plugin->version; | |||
| item->text = "Version: v" + manufacturer->version; | |||
| menu->pushChild(item); | |||
| } | |||
| @@ -452,10 +452,10 @@ void RackWidget::onMouseDownOpaque(int button) { | |||
| MenuLabel *menuLabel = new MenuLabel(); | |||
| menuLabel->text = "Add module"; | |||
| menu->pushChild(menuLabel); | |||
| for (Plugin *plugin : gPlugins) { | |||
| AddPluginMenuItem *item = new AddPluginMenuItem(); | |||
| item->text = plugin->name; | |||
| item->plugin = plugin; | |||
| for (Manufacturer *manufacturer : gManufacturers) { | |||
| AddManufacturerMenuItem *item = new AddManufacturerMenuItem(); | |||
| item->text = manufacturer->name; | |||
| item->manufacturer = manufacturer; | |||
| item->modulePos = modulePos; | |||
| menu->pushChild(item); | |||
| } | |||
| @@ -88,10 +88,10 @@ std::string assetLocal(std::string filename) { | |||
| return path; | |||
| } | |||
| std::string assetPlugin(Plugin *plugin, std::string filename) { | |||
| assert(plugin); | |||
| std::string assetManufacturer(Manufacturer *manufacturer, std::string filename) { | |||
| assert(manufacturer); | |||
| std::string path; | |||
| path = plugin->path + "/" + filename; | |||
| path = manufacturer->path + "/" + filename; | |||
| return path; | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -2,15 +2,16 @@ | |||
| #include "MidiIO.hpp" | |||
| void init(rack::Plugin *plugin) { | |||
| plugin->slug = "Core"; | |||
| plugin->name = "Core"; | |||
| plugin->homepageUrl = "https://vcvrack.com/"; | |||
| createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface"); | |||
| createModel<MidiToCVWidget>(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface"); | |||
| createModel<MIDICCToCVWidget>(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface"); | |||
| createModel<MIDIClockToCVWidget>(plugin, "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface"); | |||
| createModel<MIDITriggerToCVWidget>(plugin, "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface"); | |||
| // createModel<BridgeWidget>(plugin, "Bridge", "Bridge"); | |||
| createModel<BlankWidget>(plugin, "Blank", "Blank"); | |||
| void init(rack::Manufacturer *m) { | |||
| m->slug = "Core"; | |||
| m->name = "Core"; | |||
| m->homepageUrl = "https://vcvrack.com/"; | |||
| m->addModel(createModel<AudioInterfaceWidget>("AudioInterface", "Audio Interface")); | |||
| m->addModel(createModel<MidiToCVWidget>("MIDIToCVInterface", "MIDI-to-CV Interface")); | |||
| m->addModel(createModel<MIDICCToCVWidget>("MIDICCToCVInterface", "MIDI CC-to-CV Interface")); | |||
| m->addModel(createModel<MIDIClockToCVWidget>("MIDIClockToCVInterface", "MIDI Clock-to-CV Interface")); | |||
| m->addModel(createModel<MIDITriggerToCVWidget>("MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface")); | |||
| // m->addModel(createModel<BridgeWidget>("Bridge", "Bridge")); | |||
| m->addModel(createModel<BlankWidget>("Blank", "Blank")); | |||
| m->addModel(createModel<NotesWidget>("Notes", "Notes")); | |||
| } | |||
| @@ -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; | |||
| }; | |||
| @@ -28,7 +28,7 @@ | |||
| namespace rack { | |||
| std::list<Plugin*> gPlugins; | |||
| std::list<Manufacturer*> gManufacturers; | |||
| std::string gToken; | |||
| static bool isDownloading = false; | |||
| @@ -38,12 +38,19 @@ static std::string loginStatus; | |||
| Plugin::~Plugin() { | |||
| Manufacturer::~Manufacturer() { | |||
| for (Model *model : models) { | |||
| delete model; | |||
| } | |||
| } | |||
| void Manufacturer::addModel(Model *model) { | |||
| assert(!model->manufacturer); | |||
| model->manufacturer = this; | |||
| models.push_back(model); | |||
| } | |||
| static int loadPlugin(std::string path) { | |||
| std::string libraryFilename; | |||
| #if ARCH_LIN | |||
| @@ -70,8 +77,8 @@ static int loadPlugin(std::string path) { | |||
| } | |||
| #endif | |||
| // Call plugin init() function | |||
| typedef void (*InitCallback)(Plugin *); | |||
| // Call plugin's init() function | |||
| typedef void (*InitCallback)(Manufacturer *); | |||
| InitCallback initCallback; | |||
| #if ARCH_WIN | |||
| initCallback = (InitCallback) GetProcAddress(handle, "init"); | |||
| @@ -83,27 +90,23 @@ static int loadPlugin(std::string path) { | |||
| return -2; | |||
| } | |||
| // Construct and initialize Plugin instance | |||
| Plugin *plugin = new Plugin(); | |||
| plugin->path = path; | |||
| plugin->handle = handle; | |||
| initCallback(plugin); | |||
| // Construct and initialize Manufacturer instance | |||
| Manufacturer *manufacturer = new Manufacturer(); | |||
| manufacturer->path = path; | |||
| manufacturer->handle = handle; | |||
| initCallback(manufacturer); | |||
| // Check that this is a unique slug | |||
| for (Plugin *otherPlugin : gPlugins) { | |||
| assert(plugin->slug != otherPlugin->slug); | |||
| for (Manufacturer *otherManufacturer : gManufacturers) { | |||
| assert(manufacturer->slug != otherManufacturer->slug); | |||
| } | |||
| // Add plugin to list | |||
| gPlugins.push_back(plugin); | |||
| // Add manufacturer to list | |||
| gManufacturers.push_back(manufacturer); | |||
| 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 +118,6 @@ static void loadPlugins(std::string path) { | |||
| } | |||
| closedir(dir); | |||
| } | |||
| // Sort plugins | |||
| gPlugins.sort(comparePlugins); | |||
| } | |||
| //////////////////// | |||
| @@ -201,8 +201,8 @@ static void refreshPurchase(json_t *pluginJ) { | |||
| url += gToken; | |||
| // Find slug in plugins list, and return silently if slug already exists | |||
| for (Plugin *p : gPlugins) { | |||
| if (p->slug == slug) { | |||
| for (Manufacturer *m : gManufacturers) { | |||
| if (m->slug == slug) { | |||
| return; | |||
| } | |||
| } | |||
| @@ -237,9 +237,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); | |||
| Manufacturer *coreManufacturer = new Manufacturer(); | |||
| init(coreManufacturer); | |||
| gManufacturers.push_back(coreManufacturer); | |||
| // Load plugins from global directory | |||
| std::string globalPlugins = assetGlobal("plugins"); | |||
| @@ -255,20 +255,20 @@ void pluginInit() { | |||
| } | |||
| void pluginDestroy() { | |||
| for (Plugin *plugin : gPlugins) { | |||
| for (Manufacturer *manufacturer : gManufacturers) { | |||
| // Free library handle | |||
| #if ARCH_WIN | |||
| if (plugin->handle) | |||
| FreeLibrary((HINSTANCE)plugin->handle); | |||
| if (manufacturer->handle) | |||
| FreeLibrary((HINSTANCE)manufacturer->handle); | |||
| #elif ARCH_LIN || ARCH_MAC | |||
| if (plugin->handle) | |||
| dlclose(plugin->handle); | |||
| if (manufacturer->handle) | |||
| dlclose(manufacturer->handle); | |||
| #endif | |||
| // For some reason this segfaults | |||
| // delete plugin; | |||
| // delete manufacturer; | |||
| } | |||
| gPlugins.clear(); | |||
| gManufacturers.clear(); | |||
| } | |||
| void pluginLogIn(std::string email, std::string password) { | |||
| @@ -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); | |||
| @@ -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()); | |||