#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rack { namespace app { namespace menuBar { struct MenuButton : ui::Button { void step() override { box.size.x = bndLabelWidth(APP->window->vg, -1, text.c_str()) + 1.0; Widget::step(); } void draw(const DrawArgs& args) override { BNDwidgetState state = BND_DEFAULT; if (APP->event->hoveredWidget == this) state = BND_HOVER; if (APP->event->draggedWidget == this) state = BND_ACTIVE; bndMenuItem(args.vg, 0.0, 0.0, box.size.x, box.size.y, state, -1, text.c_str()); Widget::draw(args); } }; struct NotificationIcon : widget::Widget { void draw(const DrawArgs& args) override { nvgBeginPath(args.vg); float radius = 4; nvgCircle(args.vg, radius, radius, radius); nvgFillColor(args.vg, nvgRGBf(1.0, 0.0, 0.0)); nvgFill(args.vg); nvgStrokeColor(args.vg, nvgRGBf(0.5, 0.0, 0.0)); nvgStroke(args.vg); } }; //////////////////// // File //////////////////// struct FileButton : MenuButton { void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->addChild(createMenuItem(string::translate("MenuBar.file.new"), widget::getKeyCommandName(GLFW_KEY_N, RACK_MOD_CTRL), []() { APP->patch->loadTemplateDialog(); })); menu->addChild(createMenuItem(string::translate("MenuBar.file.open"), widget::getKeyCommandName(GLFW_KEY_O, RACK_MOD_CTRL), []() { APP->patch->loadDialog(); })); menu->addChild(createSubmenuItem(string::translate("MenuBar.file.openRecent"), "", [](ui::Menu* menu) { for (const std::string& path : settings::recentPatchPaths) { std::string name = system::getStem(path); menu->addChild(createMenuItem(name, "", [=]() { APP->patch->loadPathDialog(path); })); } }, settings::recentPatchPaths.empty())); menu->addChild(createMenuItem(string::translate("MenuBar.file.save"), widget::getKeyCommandName(GLFW_KEY_S, RACK_MOD_CTRL), []() { APP->patch->saveDialog(); })); menu->addChild(createMenuItem(string::translate("MenuBar.file.saveAs"), widget::getKeyCommandName(GLFW_KEY_S, RACK_MOD_CTRL | GLFW_MOD_SHIFT), []() { APP->patch->saveAsDialog(); })); menu->addChild(createMenuItem(string::translate("MenuBar.file.saveCopy"), "", []() { APP->patch->saveAsDialog(false); })); menu->addChild(createMenuItem(string::translate("MenuBar.file.revert"), widget::getKeyCommandName(GLFW_KEY_O, RACK_MOD_CTRL | GLFW_MOD_SHIFT), []() { APP->patch->revertDialog(); }, APP->patch->path == "")); menu->addChild(createMenuItem(string::translate("MenuBar.file.overwriteTemplate"), "", []() { APP->patch->saveTemplateDialog(); })); menu->addChild(new ui::MenuSeparator); // Load selection menu->addChild(createMenuItem(string::translate("MenuBar.file.importSelection"), "", [=]() { APP->scene->rack->loadSelectionDialog(); }, false, true)); menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuItem(string::translate("MenuBar.file.quit"), widget::getKeyCommandName(GLFW_KEY_Q, RACK_MOD_CTRL), []() { APP->window->close(); })); } }; //////////////////// // Edit //////////////////// struct EditButton : MenuButton { void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); struct UndoItem : ui::MenuItem { void step() override { bool canUndo = APP->history->canUndo(); text = canUndo ? string::f(string::translate("MenuBar.edit.undoAction"), APP->history->getUndoName()) : string::translate("MenuBar.edit.undo"); disabled = !canUndo; MenuItem::step(); } void onAction(const ActionEvent& e) override { APP->history->undo(); } }; menu->addChild(createMenuItem("", widget::getKeyCommandName(GLFW_KEY_Z, RACK_MOD_CTRL))); struct RedoItem : ui::MenuItem { void step() override { bool canRedo = APP->history->canRedo(); text = canRedo ? string::f(string::translate("MenuBar.edit.redoAction"), APP->history->getRedoName()) : string::translate("MenuBar.edit.redo"); disabled = !canRedo; MenuItem::step(); } void onAction(const ActionEvent& e) override { APP->history->redo(); } }; menu->addChild(createMenuItem("", widget::getKeyCommandName(GLFW_KEY_Z, RACK_MOD_CTRL | GLFW_MOD_SHIFT))); menu->addChild(createMenuItem(string::translate("MenuBar.edit.clearCables"), "", [=]() { APP->patch->disconnectDialog(); })); menu->addChild(new ui::MenuSeparator); APP->scene->rack->appendSelectionContextMenu(menu); } }; //////////////////// // View //////////////////// struct ZoomQuantity : Quantity { void setValue(float value) override { APP->scene->rackScroll->setZoom(std::pow(2.f, value)); } float getValue() override { return std::log2(APP->scene->rackScroll->getZoom()); } float getMinValue() override { return -2.f; } float getMaxValue() override { return 2.f; } float getDefaultValue() override { return 0.0; } float getDisplayValue() override { return std::round(std::pow(2.f, getValue()) * 100); } void setDisplayValue(float displayValue) override { setValue(std::log2(displayValue / 100)); } std::string getLabel() override { return string::translate("MenuBar.view.zoom"); } std::string getUnit() override { return "%"; } }; struct ZoomSlider : ui::Slider { ZoomSlider() { quantity = new ZoomQuantity; } ~ZoomSlider() { delete quantity; } }; struct CableOpacityQuantity : Quantity { void setValue(float value) override { settings::cableOpacity = math::clamp(value, getMinValue(), getMaxValue()); } float getValue() override { return settings::cableOpacity; } float getDefaultValue() override { return 0.5; } float getDisplayValue() override { return getValue() * 100; } void setDisplayValue(float displayValue) override { setValue(displayValue / 100); } std::string getLabel() override { return string::translate("MenuBar.view.cableOpacity"); } std::string getUnit() override { return "%"; } }; struct CableOpacitySlider : ui::Slider { CableOpacitySlider() { quantity = new CableOpacityQuantity; } ~CableOpacitySlider() { delete quantity; } }; struct CableTensionQuantity : Quantity { void setValue(float value) override { settings::cableTension = math::clamp(value, getMinValue(), getMaxValue()); } float getValue() override { return settings::cableTension; } float getDefaultValue() override { return 0.5; } float getDisplayValue() override { return getValue() * 100; } void setDisplayValue(float displayValue) override { setValue(displayValue / 100); } std::string getLabel() override { return string::translate("MenuBar.view.cableTension"); } std::string getUnit() override { return "%"; } }; struct CableTensionSlider : ui::Slider { CableTensionSlider() { quantity = new CableTensionQuantity; } ~CableTensionSlider() { delete quantity; } }; struct RackBrightnessQuantity : Quantity { void setValue(float value) override { settings::rackBrightness = math::clamp(value, getMinValue(), getMaxValue()); } float getValue() override { return settings::rackBrightness; } float getDefaultValue() override { return 1.0; } float getDisplayValue() override { return getValue() * 100; } void setDisplayValue(float displayValue) override { setValue(displayValue / 100); } std::string getUnit() override { return "%"; } std::string getLabel() override { return string::translate("MenuBar.view.roomBrightness"); } int getDisplayPrecision() override { return 3; } }; struct RackBrightnessSlider : ui::Slider { RackBrightnessSlider() { quantity = new RackBrightnessQuantity; } ~RackBrightnessSlider() { delete quantity; } }; struct HaloBrightnessQuantity : Quantity { void setValue(float value) override { settings::haloBrightness = math::clamp(value, getMinValue(), getMaxValue()); } float getValue() override { return settings::haloBrightness; } float getDefaultValue() override { return 0.25; } float getDisplayValue() override { return getValue() * 100; } void setDisplayValue(float displayValue) override { setValue(displayValue / 100); } std::string getUnit() override { return "%"; } std::string getLabel() override { return string::translate("MenuBar.view.lightBloom"); } int getDisplayPrecision() override { return 3; } }; struct HaloBrightnessSlider : ui::Slider { HaloBrightnessSlider() { quantity = new HaloBrightnessQuantity; } ~HaloBrightnessSlider() { delete quantity; } }; struct KnobScrollSensitivityQuantity : Quantity { void setValue(float value) override { value = math::clamp(value, getMinValue(), getMaxValue()); settings::knobScrollSensitivity = std::pow(2.f, value); } float getValue() override { return std::log2(settings::knobScrollSensitivity); } float getMinValue() override { return std::log2(1e-4f); } float getMaxValue() override { return std::log2(1e-2f); } float getDefaultValue() override { return std::log2(1e-3f); } float getDisplayValue() override { return std::pow(2.f, getValue() - getDefaultValue()); } void setDisplayValue(float displayValue) override { setValue(std::log2(displayValue) + getDefaultValue()); } std::string getLabel() override { return string::translate("MenuBar.view.wheelSensitivity"); } int getDisplayPrecision() override { return 2; } }; struct KnobScrollSensitivitySlider : ui::Slider { KnobScrollSensitivitySlider() { quantity = new KnobScrollSensitivityQuantity; } ~KnobScrollSensitivitySlider() { delete quantity; } }; struct ViewButton : MenuButton { void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->addChild(createMenuLabel(string::translate("MenuBar.view.window"))); bool fullscreen = APP->window->isFullScreen(); std::string fullscreenText = widget::getKeyCommandName(GLFW_KEY_F11, 0); if (fullscreen) fullscreenText += " " CHECKMARK_STRING; menu->addChild(createMenuItem(string::translate("MenuBar.view.fullscreen"), fullscreenText, [=]() { APP->window->setFullScreen(!fullscreen); })); menu->addChild(createSubmenuItem(string::translate("MenuBar.view.frameRate"), string::f("%.0f Hz", settings::frameRateLimit), [=](ui::Menu* menu) { for (int i = 1; i <= 6; i++) { double frameRate = APP->window->getMonitorRefreshRate() / i; menu->addChild(createCheckMenuItem(string::f("%.0f Hz", frameRate), "", [=]() {return settings::frameRateLimit == frameRate;}, [=]() {settings::frameRateLimit = frameRate;} )); } })); menu->addChild(createIndexPtrSubmenuItem(string::translate("MenuBar.view.pixelRatio"), { string::translate("MenuBar.view.pixelRatio.auto"), "100%", "200%", "300%" }, &settings::pixelRatio)); ZoomSlider* zoomSlider = new ZoomSlider; zoomSlider->box.size.x = 250.0; menu->addChild(zoomSlider); menu->addChild(createMenuItem(string::translate("MenuBar.view.zoomFit"), widget::getKeyCommandName(GLFW_KEY_F4, 0), [=]() { APP->scene->rackScroll->zoomToModules(); })); menu->addChild(createIndexPtrSubmenuItem(string::translate("MenuBar.view.mouseWheelZoom"), { string::f(string::translate("MenuBar.view.mouseWheelZoom.scroll"), RACK_MOD_CTRL_NAME), string::f(string::translate("MenuBar.view.mouseWheelZoom.zoom"), RACK_MOD_CTRL_NAME) }, &settings::mouseWheelZoom)); menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuLabel(string::translate("MenuBar.view.appearance"))); static const std::vector uiThemes = {"dark", "light", "hcdark"}; menu->addChild(createIndexSubmenuItem(string::translate("MenuBar.view.uiTheme"), { string::translate("MenuBar.view.appearance.dark"), string::translate("MenuBar.view.appearance.light"), string::translate("MenuBar.view.appearance.hcdark") }, [=]() -> size_t { auto it = std::find(uiThemes.begin(), uiThemes.end(), settings::uiTheme); if (it == uiThemes.end()) return -1; return it - uiThemes.begin(); }, [=](size_t i) { settings::uiTheme = uiThemes[i]; ui::refreshTheme(); })); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.showTooltips"), "", &settings::tooltips)); // Various sliders CableOpacitySlider* cableOpacitySlider = new CableOpacitySlider; cableOpacitySlider->box.size.x = 250.0; menu->addChild(cableOpacitySlider); CableTensionSlider* cableTensionSlider = new CableTensionSlider; cableTensionSlider->box.size.x = 250.0; menu->addChild(cableTensionSlider); RackBrightnessSlider* rackBrightnessSlider = new RackBrightnessSlider; rackBrightnessSlider->box.size.x = 250.0; menu->addChild(rackBrightnessSlider); HaloBrightnessSlider* haloBrightnessSlider = new HaloBrightnessSlider; haloBrightnessSlider->box.size.x = 250.0; menu->addChild(haloBrightnessSlider); // Cable colors menu->addChild(createSubmenuItem(string::translate("MenuBar.view.cableColors"), "", [=](ui::Menu* menu) { // TODO Subclass Menu to make an auto-refreshing list so user can Ctrl+click to keep menu open. // Add color items for (size_t i = 0; i < settings::cableColors.size(); i++) { NVGcolor color = settings::cableColors[i]; std::string label = get(settings::cableLabels, i); std::string labelFallback = (label != "") ? label : string::f("Color #%lld", (long long) (i + 1)); ui::ColorDotMenuItem* item = createSubmenuItem(labelFallback, "", [=](ui::Menu* menu) { // Helper for launching color dialog auto selectColor = [](NVGcolor& color) -> bool { osdialog_color c = { uint8_t(color.r * 255.f), uint8_t(color.g * 255.f), uint8_t(color.b * 255.f), uint8_t(color.a * 255.f), }; if (!osdialog_color_picker(&c, false)) return false; color = nvgRGBA(c.r, c.g, c.b, c.a); return true; }; menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.setLabel"), "", [=]() { if (i >= settings::cableColors.size()) return; char* s = osdialog_prompt(OSDIALOG_INFO, "", label.c_str()); if (!s) return; settings::cableLabels.resize(settings::cableColors.size()); settings::cableLabels[i] = s; free(s); }, false, true)); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.setColor"), "", [=]() { if (i >= settings::cableColors.size()) return; NVGcolor newColor = color; if (!selectColor(newColor)) return; std::memcpy(&settings::cableColors[i], &newColor, sizeof(newColor)); }, false, true)); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.newColorAbove"), "", [=]() { if (i >= settings::cableColors.size()) return; NVGcolor newColor = color; if (!selectColor(newColor)) return; settings::cableLabels.resize(settings::cableColors.size()); settings::cableColors.insert(settings::cableColors.begin() + i, newColor); settings::cableLabels.insert(settings::cableLabels.begin() + i, ""); }, false, true)); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.newColorBelow"), "", [=]() { if (i >= settings::cableColors.size()) return; NVGcolor newColor = color; if (!selectColor(newColor)) return; settings::cableLabels.resize(settings::cableColors.size()); settings::cableColors.insert(settings::cableColors.begin() + i + 1, newColor); settings::cableLabels.insert(settings::cableLabels.begin() + i + 1, ""); }, false, true)); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.moveUp"), "", [=]() { if (i < 1 || i >= settings::cableColors.size()) return; settings::cableLabels.resize(settings::cableColors.size()); std::swap(settings::cableColors[i], settings::cableColors[i - 1]); std::swap(settings::cableLabels[i], settings::cableLabels[i - 1]); }, i < 1, true)); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.moveDown"), "", [=]() { if (i + 1 >= settings::cableColors.size()) return; settings::cableLabels.resize(settings::cableColors.size()); std::swap(settings::cableColors[i], settings::cableColors[i + 1]); std::swap(settings::cableLabels[i], settings::cableLabels[i + 1]); }, i + 1 >= settings::cableColors.size())); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.delete"), "", [=]() { if (i >= settings::cableColors.size()) return; settings::cableLabels.resize(settings::cableColors.size()); settings::cableColors.erase(settings::cableColors.begin() + i); settings::cableLabels.erase(settings::cableLabels.begin() + i); }, settings::cableColors.size() <= 1, true)); }); item->color = color; menu->addChild(item); } menu->addChild(createBoolMenuItem(string::translate("MenuBar.view.cableColors.autoRotate"), "", [=]() -> bool { return settings::cableAutoRotate; }, [=](bool s) { settings::cableAutoRotate = s; } )); menu->addChild(createMenuItem(string::translate("MenuBar.view.cableColors.restoreFactory"), "", [=]() { if (!osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK_CANCEL, string::translate("MenuBar.view.cableColors.overwriteFactory").c_str())) return; settings::resetCables(); }, false, true)); })); menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuLabel(string::translate("MenuBar.view.parameters"))); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.lockCursor"), "", &settings::allowCursorLock)); static const std::vector knobModeLabels = { string::translate("MenuBar.view.knob.linear"), string::translate("MenuBar.view.knob.scaledLinear"), string::translate("MenuBar.view.knob.absRotary"), string::translate("MenuBar.view.knob.relRotary"), }; static const std::vector knobModes = {0, 2, 3}; menu->addChild(createSubmenuItem(string::translate("MenuBar.view.knob"), knobModeLabels[settings::knobMode], [=](ui::Menu* menu) { for (int knobMode : knobModes) { menu->addChild(createCheckMenuItem(knobModeLabels[knobMode], "", [=]() {return settings::knobMode == knobMode;}, [=]() {settings::knobMode = (settings::KnobMode) knobMode;} )); } })); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.knobScroll"), "", &settings::knobScroll)); KnobScrollSensitivitySlider* knobScrollSensitivitySlider = new KnobScrollSensitivitySlider; knobScrollSensitivitySlider->box.size.x = 250.0; menu->addChild(knobScrollSensitivitySlider); menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuLabel(string::translate("MenuBar.view.modules"))); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.lockModules"), "", &settings::lockModules)); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.squeezeModules"), "", &settings::squeezeModules)); menu->addChild(createBoolPtrMenuItem(string::translate("MenuBar.view.preferDarkPanels"), "", &settings::preferDarkPanels)); } }; //////////////////// // Engine //////////////////// struct SampleRateItem : ui::MenuItem { ui::Menu* createChildMenu() override { ui::Menu* menu = new ui::Menu; // Auto sample rate std::string rightText; if (settings::sampleRate == 0) { float sampleRate = APP->engine->getSampleRate(); rightText += string::f("(%g kHz) ", sampleRate / 1000.f); } menu->addChild(createCheckMenuItem(string::translate("MenuBar.engine.sampleRate.auto"), rightText, [=]() {return settings::sampleRate == 0;}, [=]() {settings::sampleRate = 0;} )); // Power-of-2 oversample times 44.1kHz or 48kHz for (int i = -2; i <= 4; i++) { for (int j = 0; j < 2; j++) { float oversample = std::pow(2.f, i); float sampleRate = (j == 0) ? 44100.f : 48000.f; sampleRate *= oversample; std::string text = string::f("%g kHz", sampleRate / 1000.f); std::string rightText; if (oversample > 1.f) { rightText += string::f("(%.0fx)", oversample); } else if (oversample < 1.f) { rightText += string::f("(1/%.0fx)", 1.f / oversample); } menu->addChild(createCheckMenuItem(text, rightText, [=]() {return settings::sampleRate == sampleRate;}, [=]() {settings::sampleRate = sampleRate;} )); } } return menu; } }; struct EngineButton : MenuButton { void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); std::string cpuMeterText = widget::getKeyCommandName(GLFW_KEY_F3, 0); if (settings::cpuMeter) cpuMeterText += " " CHECKMARK_STRING; menu->addChild(createMenuItem(string::translate("MenuBar.engine.cpuMeter"), cpuMeterText, [=]() { settings::cpuMeter ^= true; })); menu->addChild(createMenuItem(string::translate("MenuBar.engine.sampleRate"), RIGHT_ARROW)); menu->addChild(createSubmenuItem(string::translate("MenuBar.engine.threads"), string::f("%d", settings::threadCount), [=](ui::Menu* menu) { // BUG This assumes SMT is enabled. int cores = system::getLogicalCoreCount() / 2; for (int i = 1; i <= 2 * cores; i++) { std::string rightText; if (i == cores) rightText += string::translate("MenuBar.engine.threads.most"); else if (i == 1) rightText += string::translate("MenuBar.engine.threads.lowest"); menu->addChild(createCheckMenuItem(string::f("%d", i), rightText, [=]() {return settings::threadCount == i;}, [=]() {settings::threadCount = i;} )); } })); } }; //////////////////// // Plugins //////////////////// struct AccountPasswordField : ui::PasswordField { ui::MenuItem* logInItem; void onAction(const ActionEvent& e) override { logInItem->doAction(); } }; struct LogInItem : ui::MenuItem { ui::TextField* emailField; ui::TextField* passwordField; void onAction(const ActionEvent& e) override { std::string email = emailField->text; std::string password = passwordField->text; std::thread t([=] { library::logIn(email, password); library::checkUpdates(); }); t.detach(); e.unconsume(); } void step() override { text = string::translate("MenuBar.library.login"); rightText = library::loginStatus; MenuItem::step(); } }; struct SyncUpdatesItem : ui::MenuItem { void step() override { if (library::updateStatus != "") { text = library::updateStatus; } else if (library::isSyncing) { text = string::translate("MenuBar.library.updating"); } else if (!library::hasUpdates()) { text = string::translate("MenuBar.library.upToDate"); } else { text = string::translate("MenuBar.library.updateAll"); } disabled = library::isSyncing || !library::hasUpdates(); MenuItem::step(); } void onAction(const ActionEvent& e) override { std::thread t([=] { library::syncUpdates(); }); t.detach(); e.unconsume(); } }; struct SyncUpdateItem : ui::MenuItem { std::string slug; void setUpdate(const std::string& slug) { this->slug = slug; auto it = library::updateInfos.find(slug); if (it == library::updateInfos.end()) return; library::UpdateInfo update = it->second; text = update.name; } ui::Menu* createChildMenu() override { auto it = library::updateInfos.find(slug); if (it == library::updateInfos.end()) return NULL; library::UpdateInfo update = it->second; ui::Menu* menu = new ui::Menu; if (update.minRackVersion != "") { menu->addChild(createMenuLabel(string::f(string::translate("MenuBar.library.requiresRack"), update.minRackVersion))); } if (update.changelogUrl != "") { std::string changelogUrl = update.changelogUrl; menu->addChild(createMenuItem(string::translate("MenuBar.library.changelog"), "", [=]() { system::openBrowser(changelogUrl); })); } if (menu->children.empty()) { delete menu; return NULL; } return menu; } void step() override { disabled = false; if (library::isSyncing) disabled = true; auto it = library::updateInfos.find(slug); if (it == library::updateInfos.end()) { disabled = true; } else { library::UpdateInfo update = it->second; if (update.minRackVersion != "") disabled = true; if (update.downloaded) { rightText = CHECKMARK_STRING; disabled = true; } else if (slug == library::updateSlug) { rightText = string::f("%.0f%%", library::updateProgress * 100.f); } else { rightText = ""; plugin::Plugin* p = plugin::getPlugin(slug); if (p) { rightText += p->version + " → "; } rightText += update.version; } } MenuItem::step(); } void onAction(const ActionEvent& e) override { std::thread t([=] { library::syncUpdate(slug); }); t.detach(); e.unconsume(); } }; struct LibraryMenu : ui::Menu { LibraryMenu() { refresh(); } void step() override { // Refresh menu when appropriate if (library::refreshRequested) { library::refreshRequested = false; refresh(); } Menu::step(); } void refresh() { setChildMenu(NULL); clearChildren(); if (settings::devMode) { addChild(createMenuLabel(string::translate("MenuBar.library.devMode"))); } else if (!library::isLoggedIn()) { addChild(createMenuItem(string::translate("MenuBar.library.register"), "", [=]() { system::openBrowser("https://vcvrack.com/login"); })); ui::TextField* emailField = new ui::TextField; emailField->placeholder = string::translate("MenuBar.library.email"); emailField->box.size.x = 240.0; addChild(emailField); AccountPasswordField* passwordField = new AccountPasswordField; passwordField->placeholder = string::translate("MenuBar.library.password"); passwordField->box.size.x = 240.0; passwordField->nextField = emailField; emailField->nextField = passwordField; addChild(passwordField); LogInItem* logInItem = new LogInItem; logInItem->emailField = emailField; logInItem->passwordField = passwordField; passwordField->logInItem = logInItem; addChild(logInItem); } else { addChild(createMenuItem(string::translate("MenuBar.library.logOut"), "", [=]() { library::logOut(); })); addChild(createMenuItem(string::translate("MenuBar.library.account"), "", [=]() { system::openBrowser("https://vcvrack.com/account"); })); addChild(createMenuItem(string::translate("MenuBar.library.browse"), "", [=]() { system::openBrowser("https://library.vcvrack.com/"); })); SyncUpdatesItem* syncItem = new SyncUpdatesItem; syncItem->text = string::translate("MenuBar.library.updateAll"); addChild(syncItem); if (!library::updateInfos.empty()) { addChild(new ui::MenuSeparator); addChild(createMenuLabel(string::translate("MenuBar.library.updates"))); for (auto& pair : library::updateInfos) { SyncUpdateItem* updateItem = new SyncUpdateItem; updateItem->setUpdate(pair.first); addChild(updateItem); } } } } }; struct LibraryButton : MenuButton { NotificationIcon* notification; LibraryButton() { notification = new NotificationIcon; addChild(notification); } void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); // Check for updates when menu is opened if (!settings::devMode) { std::thread t([&]() { system::setThreadName(string::translate("MenuBar.library")); library::checkUpdates(); }); t.detach(); } } void step() override { notification->box.pos = math::Vec(0, 0); notification->visible = library::hasUpdates(); // Popup when updates finish downloading if (library::restartRequested) { library::restartRequested = false; if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, string::translate("MenuBar.library.restart").c_str())) { APP->window->close(); settings::restart = true; } } MenuButton::step(); } }; //////////////////// // Help //////////////////// struct HelpButton : MenuButton { NotificationIcon* notification; HelpButton() { notification = new NotificationIcon; addChild(notification); } void onAction(const ActionEvent& e) override { ui::Menu* menu = createMenu(); menu->cornerFlags = BND_CORNER_TOP; menu->box.pos = getAbsoluteOffset(math::Vec(0, box.size.y)); menu->addChild(createSubmenuItem("🌐 " + string::translate("MenuBar.help.language"), "", [=](ui::Menu* menu) { appendLanguageMenu(menu); })); menu->addChild(createMenuItem(string::translate("MenuBar.help.tips"), "", [=]() { APP->scene->addChild(tipWindowCreate()); })); menu->addChild(createMenuItem(string::translate("MenuBar.help.manual"), widget::getKeyCommandName(GLFW_KEY_F1, 0), [=]() { system::openBrowser("https://vcvrack.com/manual"); })); menu->addChild(createMenuItem(string::translate("MenuBar.help.support"), "", [=]() { system::openBrowser("https://vcvrack.com/support"); })); menu->addChild(createMenuItem("VCVRack.com", "", [=]() { system::openBrowser("https://vcvrack.com/"); })); menu->addChild(new ui::MenuSeparator); menu->addChild(createMenuItem(string::translate("MenuBar.help.userFolder"), "", [=]() { system::openDirectory(asset::user("")); })); menu->addChild(createMenuItem(string::translate("MenuBar.help.changelog"), "", [=]() { system::openBrowser("https://github.com/VCVRack/Rack/blob/v2/CHANGELOG.md"); })); if (library::isAppUpdateAvailable()) { menu->addChild(createMenuItem(string::f(string::translate("MenuBar.help.update"), APP_NAME), APP_VERSION + " → " + library::appVersion, [=]() { system::openBrowser(library::appDownloadUrl); })); } else if (!settings::autoCheckUpdates && !settings::devMode) { menu->addChild(createMenuItem(string::f(string::translate("MenuBar.help.checkUpdate"), APP_NAME), "", [=]() { std::thread t([&]() { library::checkAppUpdate(); }); t.detach(); }, false, true)); } } void step() override { notification->box.pos = math::Vec(0, 0); notification->visible = library::isAppUpdateAvailable(); MenuButton::step(); } }; //////////////////// // MenuBar //////////////////// struct InfoLabel : ui::Label { int frameCount = 0; double frameDurationTotal = 0.0; double frameDurationAvg = NAN; // double uiLastTime = 0.0; // double uiLastThreadTime = 0.0; // double uiFrac = 0.0; void step() override { // Compute frame rate double frameDuration = APP->window->getLastFrameDuration(); if (std::isfinite(frameDuration)) { frameDurationTotal += frameDuration; frameCount++; } if (frameDurationTotal >= 1.0) { frameDurationAvg = frameDurationTotal / frameCount; frameDurationTotal = 0.0; frameCount = 0; } // Compute UI thread CPU // double time = system::getTime(); // double uiDuration = time - uiLastTime; // if (uiDuration >= 1.0) { // double threadTime = system::getThreadTime(); // uiFrac = (threadTime - uiLastThreadTime) / uiDuration; // uiLastThreadTime = threadTime; // uiLastTime = time; // } text = ""; if (box.size.x >= 460) { double fps = std::isfinite(frameDurationAvg) ? 1.0 / frameDurationAvg : 0.0; double meterAverage = APP->engine->getMeterAverage(); double meterMax = APP->engine->getMeterMax(); text += string::f(string::translate("MenuBar.infoLabel"), fps, meterAverage * 100, meterMax * 100); text += " "; } text += APP_NAME + " " + APP_EDITION_NAME + " " + APP_VERSION + " " + APP_OS_NAME + " " + APP_CPU_NAME; Label::step(); } }; struct MenuBar : widget::OpaqueWidget { InfoLabel* infoLabel; MenuBar() { const float margin = 5; box.size.y = BND_WIDGET_HEIGHT + 2 * margin; ui::SequentialLayout* layout = new ui::SequentialLayout; layout->margin = math::Vec(margin, margin); layout->spacing = math::Vec(0, 0); addChild(layout); FileButton* fileButton = new FileButton; fileButton->text = string::translate("MenuBar.file"); layout->addChild(fileButton); EditButton* editButton = new EditButton; editButton->text = string::translate("MenuBar.edit"); layout->addChild(editButton); ViewButton* viewButton = new ViewButton; viewButton->text = string::translate("MenuBar.view"); layout->addChild(viewButton); EngineButton* engineButton = new EngineButton; engineButton->text = string::translate("MenuBar.engine"); layout->addChild(engineButton); LibraryButton* libraryButton = new LibraryButton; libraryButton->text = string::translate("MenuBar.library"); layout->addChild(libraryButton); HelpButton* helpButton = new HelpButton; helpButton->text = string::translate("MenuBar.help"); layout->addChild(helpButton); infoLabel = new InfoLabel; infoLabel->box.size.x = 600; infoLabel->alignment = ui::Label::RIGHT_ALIGNMENT; layout->addChild(infoLabel); } void draw(const DrawArgs& args) override { bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, BND_CORNER_ALL); bndBevel(args.vg, 0.0, 0.0, box.size.x, box.size.y); Widget::draw(args); } void step() override { Widget::step(); infoLabel->box.size.x = box.size.x - infoLabel->box.pos.x - 5; // Setting 50% alpha prevents Label from using the default UI theme color, so set the color manually here. infoLabel->color = color::alpha(bndGetTheme()->regularTheme.textColor, 0.5); } }; } // namespace menuBar widget::Widget* createMenuBar() { menuBar::MenuBar* menuBar = new menuBar::MenuBar; return menuBar; } void appendLanguageMenu(ui::Menu* menu) { for (const std::string& language : string::getLanguages()) { menu->addChild(createCheckMenuItem(string::translate("language", language), "", [=]() { return settings::language == language; }, [=]() { if (settings::language == language) return; settings::language = language; // Request restart std::string msg = string::f(string::translate("MenuBar.help.language.restart"), string::translate("language")); if (osdialog_message(OSDIALOG_INFO, OSDIALOG_OK_CANCEL, msg.c_str())) { APP->window->close(); settings::restart = true; } })); } } } // namespace app } // namespace rack