@@ -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 | |||
@@ -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,7 +42,6 @@ DEPS = $(patsubst %, build/%.d, $(SOURCES)) | |||
$(TARGET): $(OBJECTS) | |||
$(CXX) -o $@ $^ $(LDFLAGS) | |||
# Object targets | |||
-include $(DEPS) | |||
@@ -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); | |||
@@ -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<Model*> 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); |
@@ -18,7 +18,7 @@ namespace rack { | |||
//////////////////// | |||
template <class TModuleWidget> | |||
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; | |||
} | |||
@@ -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; | |||
@@ -24,7 +24,7 @@ endif | |||
all: $(TARGET) | |||
include ../../compile.mk | |||
clean: | |||
rm -rfv build $(TARGET) dist | |||
include ../../compile.mk |
@@ -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(); | |||
@@ -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(); | |||
} | |||
@@ -1,12 +1,13 @@ | |||
#include <map> | |||
#include <algorithm> | |||
#include <thread> | |||
#include "app.hpp" | |||
#include "engine.hpp" | |||
#include "plugin.hpp" | |||
#include "gui.hpp" | |||
#include "settings.hpp" | |||
#include "asset.hpp" | |||
#include <map> | |||
#include <algorithm> | |||
#include <thread> | |||
#include <set> | |||
#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<Model*> 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<std::string> 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); | |||
} | |||
@@ -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; | |||
} | |||
@@ -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<ScrewSilver>(Vec(15, 0))); | |||
addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0))); | |||
addChild(createScrew<ScrewSilver>(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); | |||
} |
@@ -2,15 +2,14 @@ | |||
#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::Plugin *p) { | |||
p->slug = "Core"; | |||
p->addModel(createModel<AudioInterfaceWidget>("Core", "Core", "AudioInterface", "Audio Interface")); | |||
p->addModel(createModel<MidiToCVWidget>("Core", "Core", "MIDIToCVInterface", "MIDI-to-CV Interface")); | |||
p->addModel(createModel<MIDICCToCVWidget>("Core", "Core", "MIDICCToCVInterface", "MIDI CC-to-CV Interface")); | |||
p->addModel(createModel<MIDIClockToCVWidget>("Core", "Core", "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface")); | |||
p->addModel(createModel<MIDITriggerToCVWidget>("Core", "Core", "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface")); | |||
// p->addModel(createModel<BridgeWidget>("Core", "Core", "Bridge", "Bridge")); | |||
p->addModel(createModel<BlankWidget>("Core", "Core", "Blank", "Blank")); | |||
p->addModel(createModel<NotesWidget>("Core", "Core", "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; | |||
}; |
@@ -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(); | |||
@@ -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()); | |||