@@ -18,6 +18,7 @@ struct Scene : widget::OpaqueWidget { | |||||
RackWidget* rack; | RackWidget* rack; | ||||
widget::Widget* menuBar; | widget::Widget* menuBar; | ||||
widget::Widget* moduleBrowser; | widget::Widget* moduleBrowser; | ||||
widget::Widget* tipWindow; | |||||
widget::Widget* frameRateWidget; | widget::Widget* frameRateWidget; | ||||
double lastAutosaveTime = 0.0; | double lastAutosaveTime = 0.0; | ||||
@@ -0,0 +1,14 @@ | |||||
#pragma once | |||||
#include <app/common.hpp> | |||||
#include <widget/Widget.hpp> | |||||
namespace rack { | |||||
namespace app { | |||||
widget::Widget* tipWindowCreate(); | |||||
} // namespace app | |||||
} // namespace rack |
@@ -3,6 +3,7 @@ | |||||
#include <common.hpp> | #include <common.hpp> | ||||
#include <math.hpp> | #include <math.hpp> | ||||
#include <ui/common.hpp> | |||||
namespace rack { | namespace rack { | ||||
@@ -54,6 +54,8 @@ extern std::vector<NVGcolor> cableColors; | |||||
// pluginSlug -> moduleSlugs | // pluginSlug -> moduleSlugs | ||||
extern std::map<std::string, std::set<std::string>> moduleWhitelist; | extern std::map<std::string, std::set<std::string>> moduleWhitelist; | ||||
extern bool autoCheckUpdates; | extern bool autoCheckUpdates; | ||||
extern bool showTipsOnLaunch; | |||||
extern int tipIndex; | |||||
json_t* toJson(); | json_t* toJson(); | ||||
void fromJson(json_t* rootJ); | void fromJson(json_t* rootJ); | ||||
@@ -21,7 +21,9 @@ struct SequentialLayout : widget::Widget { | |||||
Orientation orientation = HORIZONTAL_ORIENTATION; | Orientation orientation = HORIZONTAL_ORIENTATION; | ||||
Alignment alignment = LEFT_ALIGNMENT; | Alignment alignment = LEFT_ALIGNMENT; | ||||
/** Space between adjacent elements */ | |||||
/** Space between box bounds. */ | |||||
math::Vec margin; | |||||
/** Space between adjacent elements. */ | |||||
math::Vec spacing; | math::Vec spacing; | ||||
void step() override; | void step() override; | ||||
@@ -40,6 +40,7 @@ struct Widget : WeakBase { | |||||
void setPosition(math::Vec pos); | void setPosition(math::Vec pos); | ||||
math::Vec getSize(); | math::Vec getSize(); | ||||
void setSize(math::Vec size); | void setSize(math::Vec size); | ||||
widget::Widget* getParent(); | |||||
bool isVisible(); | bool isVisible(); | ||||
void setVisible(bool visible); | void setVisible(bool visible); | ||||
void show() { | void show() { | ||||
@@ -887,6 +887,13 @@ struct CheckAppUpdateItem : ui::MenuItem { | |||||
}; | }; | ||||
struct TipItem : ui::MenuItem { | |||||
void onAction(const event::Action& e) override { | |||||
APP->scene->tipWindow->show(); | |||||
} | |||||
}; | |||||
struct HelpButton : MenuButton { | struct HelpButton : MenuButton { | ||||
NotificationIcon* notification; | NotificationIcon* notification; | ||||
@@ -912,6 +919,10 @@ struct HelpButton : MenuButton { | |||||
menu->addChild(checkAppUpdateItem); | menu->addChild(checkAppUpdateItem); | ||||
} | } | ||||
TipItem* tipItem = new TipItem; | |||||
tipItem->text = "Tips"; | |||||
menu->addChild(tipItem); | |||||
UrlItem* manualItem = new UrlItem; | UrlItem* manualItem = new UrlItem; | ||||
manualItem->text = "Manual"; | manualItem->text = "Manual"; | ||||
manualItem->rightText = "F1"; | manualItem->rightText = "F1"; | ||||
@@ -4,6 +4,7 @@ | |||||
#include <app/Scene.hpp> | #include <app/Scene.hpp> | ||||
#include <app/ModuleBrowser.hpp> | #include <app/ModuleBrowser.hpp> | ||||
#include <app/TipWindow.hpp> | |||||
#include <app/MenuBar.hpp> | #include <app/MenuBar.hpp> | ||||
#include <context.hpp> | #include <context.hpp> | ||||
#include <system.hpp> | #include <system.hpp> | ||||
@@ -45,6 +46,10 @@ Scene::Scene() { | |||||
moduleBrowser->hide(); | moduleBrowser->hide(); | ||||
addChild(moduleBrowser); | addChild(moduleBrowser); | ||||
tipWindow = tipWindowCreate(); | |||||
tipWindow->setVisible(settings::showTipsOnLaunch); | |||||
addChild(tipWindow); | |||||
frameRateWidget = new FrameRateWidget; | frameRateWidget = new FrameRateWidget; | ||||
frameRateWidget->box.size = math::Vec(80.0, 30.0); | frameRateWidget->box.size = math::Vec(80.0, 30.0); | ||||
frameRateWidget->hide(); | frameRateWidget->hide(); | ||||
@@ -0,0 +1,199 @@ | |||||
#include <thread> | |||||
#include <app/TipWindow.hpp> | |||||
#include <widget/OpaqueWidget.hpp> | |||||
#include <ui/Label.hpp> | |||||
#include <ui/Button.hpp> | |||||
#include <ui/MenuItem.hpp> | |||||
#include <ui/SequentialLayout.hpp> | |||||
#include <settings.hpp> | |||||
#include <system.hpp> | |||||
namespace rack { | |||||
namespace app { | |||||
struct TipOverlay : widget::OpaqueWidget { | |||||
void step() override { | |||||
box = parent->box.zeroPos(); | |||||
OpaqueWidget::step(); | |||||
} | |||||
void onButton(const event::Button& e) override { | |||||
OpaqueWidget::onButton(e); | |||||
if (e.getTarget() != this) | |||||
return; | |||||
if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { | |||||
hide(); | |||||
e.consume(this); | |||||
} | |||||
} | |||||
}; | |||||
struct UrlButton : ui::Button { | |||||
std::string url; | |||||
void onAction(const event::Action& e) override { | |||||
std::thread t([=] { | |||||
system::openBrowser(url); | |||||
}); | |||||
t.detach(); | |||||
} | |||||
}; | |||||
struct TipInfo { | |||||
std::string text; | |||||
std::string linkText; | |||||
std::string linkUrl; | |||||
}; | |||||
static std::vector<TipInfo> tipInfos = { | |||||
{"To add a module to the rack, right-click an empty rack space or press Enter. Click and drag a module from the Module Browser into the desired rack space.\n\nYou can force-move modules by holding " RACK_MOD_CTRL_NAME " while dragging it.", "", ""}, // reviewed | |||||
{"Pan around the rack by using the scroll bars, dragging while holding the middle mouse button, or pressing the arrow keys. Arrow key panning speed can be modified by holding " RACK_MOD_CTRL_NAME ", " RACK_MOD_SHIFT_NAME ", or " RACK_MOD_CTRL_NAME "+" RACK_MOD_SHIFT_NAME ".\n\nZoom in and out using the View menu, " RACK_MOD_CTRL_NAME "+scroll, or " RACK_MOD_CTRL_NAME "+= / " RACK_MOD_CTRL_NAME "+-.", "", ""}, // reviewed | |||||
// {"Want to use VCV Rack as a plugin in your DAW? VCV Rack for DAWs is available now as a 64-bit VST 2 plugin for Ableton Live, Cubase, FL Studio, Reason, Studio One, REAPER, Bitwig, and more. Other plugin formats coming soon.", "Learn more", "https://vcvrack.com/RackForDAWs"}, // reviewed | |||||
{"You can use Rack fullscreen by selecting View > Fullscreen or pressing F11.\n\nIn fullscreen mode, the menu bar and scroll bars are hidden. This is ideal for screen recording with VCV Recorder.", "Get VCV Recorder", "https://vcvrack.com/Recorder"}, // reviewed | |||||
{"You can browse over 2400 modules on the VCV Library.", "VCV Library", "https://library.vcvrack.com/"}, | |||||
{"Some plugin developers accept donations for their work. Right-click a module panel and select Info > Donate.\n\nYou can support VCV Rack by purchasing VCV plugins.", "VCV Library", "https://library.vcvrack.com/"}, // reviewed | |||||
{"You can learn more about VCV Rack by browsing the official Rack manual.", "VCV Rack manual", "https://vcvrack.com/manual/"}, | |||||
{"Follow VCV Rack on Twitter for new modules, product announcements, and development news.", "Twitter @vcvrack", "https://twitter.com/vcvrack"}, // reviewed | |||||
// {"", "", ""}, | |||||
}; | |||||
struct TipWindow : widget::OpaqueWidget { | |||||
ui::Label* label; | |||||
UrlButton* linkButton; | |||||
TipWindow() { | |||||
float margin = 10; | |||||
float buttonWidth = 80; | |||||
box.size.x = buttonWidth*5 + margin*6; | |||||
ui::Label* header = new ui::Label; | |||||
header->box.pos.x = margin; | |||||
header->box.pos.y = 20; | |||||
// header->box.size.x = box.size.x - margin*2; | |||||
header->box.size.y = 20; | |||||
header->fontSize = 20; | |||||
header->text = "Welcome to VCV Rack v" + APP_VERSION; | |||||
addChild(header); | |||||
label = new ui::Label; | |||||
label->box.pos.x = margin; | |||||
label->box.pos.y = header->box.getBottom() + margin; | |||||
label->box.size.y = 80; | |||||
label->box.size.x = box.size.x - margin*2; | |||||
addChild(label); | |||||
linkButton = new UrlButton; | |||||
linkButton->box.pos.x = margin; | |||||
linkButton->box.pos.y = label->box.getBottom() + margin; | |||||
linkButton->box.size.x = box.size.x - margin*2; | |||||
addChild(linkButton); | |||||
ui::SequentialLayout* buttonLayout = new ui::SequentialLayout; | |||||
buttonLayout->box.pos.x = margin; | |||||
buttonLayout->box.pos.y = linkButton->box.getBottom() + margin; | |||||
buttonLayout->box.size.x = box.size.x - margin*2; | |||||
buttonLayout->spacing = math::Vec(margin, margin); | |||||
addChild(buttonLayout); | |||||
struct ShowButton : ui::Button { | |||||
void step() override { | |||||
text = settings::showTipsOnLaunch ? "Don't show at startup" : "Show tips at startup"; | |||||
} | |||||
void onAction(const event::Action& e) override { | |||||
settings::showTipsOnLaunch ^= true; | |||||
} | |||||
}; | |||||
ShowButton* showButton = new ShowButton; | |||||
showButton->box.size.x = buttonWidth * 2 + margin; | |||||
buttonLayout->addChild(showButton); | |||||
struct PreviousButton : ui::Button { | |||||
TipWindow* tipWindow; | |||||
void onAction(const event::Action& e) override { | |||||
tipWindow->advanceTip(-1); | |||||
} | |||||
}; | |||||
PreviousButton* prevButton = new PreviousButton; | |||||
prevButton->box.size.x = buttonWidth; | |||||
prevButton->text = "Previous"; | |||||
prevButton->tipWindow = this; | |||||
buttonLayout->addChild(prevButton); | |||||
struct NextButton : ui::Button { | |||||
TipWindow* tipWindow; | |||||
void onAction(const event::Action& e) override { | |||||
tipWindow->advanceTip(); | |||||
} | |||||
}; | |||||
NextButton* nextButton = new NextButton; | |||||
nextButton->box.size.x = buttonWidth; | |||||
nextButton->text = "Next"; | |||||
nextButton->tipWindow = this; | |||||
buttonLayout->addChild(nextButton); | |||||
struct CloseButton : ui::Button { | |||||
TipWindow* tipWindow; | |||||
void onAction(const event::Action& e) override { | |||||
tipWindow->getParent()->hide(); | |||||
} | |||||
}; | |||||
CloseButton* closeButton = new CloseButton; | |||||
closeButton->box.size.x = buttonWidth; | |||||
closeButton->text = "Close"; | |||||
closeButton->tipWindow = this; | |||||
buttonLayout->addChild(closeButton); | |||||
buttonLayout->box.size.y = closeButton->box.size.y; | |||||
box.size.y = buttonLayout->box.getBottom() + margin; | |||||
// When the TipWindow is created, choose the next tip | |||||
advanceTip(); | |||||
} | |||||
void advanceTip(int delta = 1) { | |||||
// Increment tip index | |||||
settings::tipIndex = math::eucMod(settings::tipIndex + delta, (int) tipInfos.size()); | |||||
TipInfo& tipInfo = tipInfos[settings::tipIndex]; | |||||
label->text = tipInfo.text; | |||||
linkButton->setVisible(tipInfo.linkText != ""); | |||||
linkButton->text = tipInfo.linkText; | |||||
linkButton->url = tipInfo.linkUrl; | |||||
} | |||||
void step() override { | |||||
box.pos = parent->box.size.minus(box.size).div(2); | |||||
OpaqueWidget::step(); | |||||
} | |||||
void draw(const DrawArgs& args) override { | |||||
bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0); | |||||
Widget::draw(args); | |||||
} | |||||
void onShow(const event::Show& e) override { | |||||
advanceTip(); | |||||
OpaqueWidget::onShow(e); | |||||
} | |||||
}; | |||||
widget::Widget* tipWindowCreate() { | |||||
TipOverlay* overlay = new TipOverlay; | |||||
TipWindow* tipWindow = new TipWindow; | |||||
overlay->addChild(tipWindow); | |||||
return overlay; | |||||
} | |||||
} // namespace app | |||||
} // namespace rack |
@@ -51,6 +51,8 @@ std::vector<NVGcolor> cableColors = { | |||||
}; | }; | ||||
std::map<std::string, std::set<std::string>> moduleWhitelist = {}; | std::map<std::string, std::set<std::string>> moduleWhitelist = {}; | ||||
bool autoCheckUpdates = true; | bool autoCheckUpdates = true; | ||||
bool showTipsOnLaunch = true; | |||||
int tipIndex = -1; | |||||
json_t* toJson() { | json_t* toJson() { | ||||
@@ -122,6 +124,10 @@ json_t* toJson() { | |||||
json_object_set_new(rootJ, "autoCheckUpdates", json_boolean(autoCheckUpdates)); | json_object_set_new(rootJ, "autoCheckUpdates", json_boolean(autoCheckUpdates)); | ||||
json_object_set_new(rootJ, "showTipsOnLaunch", json_boolean(showTipsOnLaunch)); | |||||
json_object_set_new(rootJ, "tipIndex", json_integer(tipIndex)); | |||||
return rootJ; | return rootJ; | ||||
} | } | ||||
@@ -249,6 +255,14 @@ void fromJson(json_t* rootJ) { | |||||
json_t* autoCheckUpdatesJ = json_object_get(rootJ, "autoCheckUpdates"); | json_t* autoCheckUpdatesJ = json_object_get(rootJ, "autoCheckUpdates"); | ||||
if (autoCheckUpdatesJ) | if (autoCheckUpdatesJ) | ||||
autoCheckUpdates = json_boolean_value(autoCheckUpdatesJ); | autoCheckUpdates = json_boolean_value(autoCheckUpdatesJ); | ||||
json_t* showTipsOnLaunchJ = json_object_get(rootJ, "showTipsOnLaunch"); | |||||
if (showTipsOnLaunchJ) | |||||
showTipsOnLaunch = json_boolean_value(showTipsOnLaunchJ); | |||||
json_t* tipIndexJ = json_object_get(rootJ, "tipIndex"); | |||||
if (tipIndexJ) | |||||
tipIndex = json_integer_value(tipIndexJ); | |||||
} | } | ||||
void save(const std::string& path) { | void save(const std::string& path) { | ||||
@@ -14,53 +14,67 @@ namespace ui { | |||||
void SequentialLayout::step() { | void SequentialLayout::step() { | ||||
Widget::step(); | Widget::step(); | ||||
// Sort widgets into rows (or columns if vertical) | |||||
std::vector<std::vector<widget::Widget*>> rows; | |||||
rows.resize(1); | |||||
float rowWidth = 0.0; | |||||
for (widget::Widget* child : children) { | |||||
if (!child->visible) | |||||
continue; | |||||
math::Rect bound; | |||||
bound.pos = margin; | |||||
bound.size = box.size.minus(margin.mult(2)); | |||||
// Should we wrap the widget now? | |||||
if (!rows.back().empty() && rowWidth + X(child->box.size) >= X(box.size)) { | |||||
rowWidth = 0.0; | |||||
rows.resize(rows.size() + 1); | |||||
} | |||||
rows.back().push_back(child); | |||||
rowWidth += X(child->box.size) + X(spacing); | |||||
} | |||||
// Position widgets | |||||
math::Vec p; | |||||
for (auto& row : rows) { | |||||
// Sort widgets into rows (or columns if vertical) | |||||
std::vector<widget::Widget*> row; | |||||
math::Vec cursor = bound.pos; | |||||
auto flushRow = [&]() { | |||||
// For center and right alignment, compute offset from the left margin | // For center and right alignment, compute offset from the left margin | ||||
float offset = 0.0; | |||||
float offset = 0.f; | |||||
if (alignment != LEFT_ALIGNMENT) { | if (alignment != LEFT_ALIGNMENT) { | ||||
float rowWidth = 0.0; | |||||
float rowWidth = 0.f; | |||||
for (widget::Widget* child : row) { | for (widget::Widget* child : row) { | ||||
rowWidth += X(child->box.size) + X(spacing); | rowWidth += X(child->box.size) + X(spacing); | ||||
} | } | ||||
rowWidth -= X(spacing); | rowWidth -= X(spacing); | ||||
if (alignment == CENTER_ALIGNMENT) | if (alignment == CENTER_ALIGNMENT) | ||||
offset = (X(box.size) - rowWidth) / 2; | |||||
offset = (X(bound.size) - rowWidth) / 2; | |||||
else if (alignment == RIGHT_ALIGNMENT) | else if (alignment == RIGHT_ALIGNMENT) | ||||
offset = X(box.size) - rowWidth; | |||||
offset = X(bound.size) - rowWidth; | |||||
} | } | ||||
float maxHeight = 0.0; | |||||
// Set positions of widgets | |||||
float maxHeight = 0.f; | |||||
for (widget::Widget* child : row) { | for (widget::Widget* child : row) { | ||||
child->box.pos = p; | |||||
child->box.pos = cursor; | |||||
X(child->box.pos) += offset; | X(child->box.pos) += offset; | ||||
X(cursor) += X(child->box.size) + X(spacing); | |||||
X(p) += X(child->box.size) + X(spacing); | |||||
if (Y(child->box.size) > maxHeight) | if (Y(child->box.size) > maxHeight) | ||||
maxHeight = Y(child->box.size); | maxHeight = Y(child->box.size); | ||||
} | } | ||||
X(p) = 0.0; | |||||
Y(p) += maxHeight + Y(spacing); | |||||
row.clear(); | |||||
// Reset cursor to next line | |||||
X(cursor) = X(bound.pos); | |||||
Y(cursor) += maxHeight + Y(spacing); | |||||
}; | |||||
// Iterate through children until row is full | |||||
float rowWidth = 0.0; | |||||
for (widget::Widget* child : children) { | |||||
if (!child->isVisible()) { | |||||
child->box.pos = math::Vec(); | |||||
continue; | |||||
} | |||||
// Should we wrap the widget now? | |||||
if (!row.empty() && rowWidth + X(child->box.size) > X(bound.size)) { | |||||
flushRow(); | |||||
rowWidth = 0.0; | |||||
} | |||||
row.push_back(child); | |||||
rowWidth += X(child->box.size) + X(spacing); | |||||
} | |||||
// Flush last row | |||||
if (!row.empty()) { | |||||
flushRow(); | |||||
} | } | ||||
} | } | ||||
@@ -56,6 +56,11 @@ void Widget::setSize(math::Vec size) { | |||||
} | } | ||||
widget::Widget *Widget::getParent() { | |||||
return parent; | |||||
} | |||||
bool Widget::isVisible() { | bool Widget::isVisible() { | ||||
return visible; | return visible; | ||||
} | } | ||||