diff --git a/design/tuning_hamburger.hpp b/design/tuning_hamburger.hpp index da5a9a8..b5e5693 100644 --- a/design/tuning_hamburger.hpp +++ b/design/tuning_hamburger.hpp @@ -37,7 +37,7 @@ struct TuningMenu : Hamburger void appendContextMenu(Menu * menu) override { if (!my_module) return; - menu->addChild(createMenuItem(describeTuning(tuning), "", [](){}, true)); + menu->addChild(createMenuLabel(describeTuning(tuning)); menu->addChild(new MenuSeparator); menu->addChild(createTuningMenuItem(Tuning::EqualTuning)); menu->addChild(createSubmenuItem("n-Tone Equal", "", [=](Menu * menu) { diff --git a/doc/HC-1.md b/doc/HC-1.md index 13848fa..0868af7 100644 --- a/doc/HC-1.md +++ b/doc/HC-1.md @@ -123,33 +123,33 @@ next to the knob, indicating which pedal is assigned to that knob. - The bottom row shows the following, from left to right: - - Midi device input and output selectors. If you have only one Eagan Matrix device, you should never need to use these, becuase HC-1 generally finds the connected Eagan Matrix device automatically. + - EM Midi device selector. If you have only one Eagan Matrix device, you should never need to use this. HC-1 finds a connected EM device automatically. - - Test note button. Click to send a Middle C (Note 60) *Note on* MIDI message, and Ctrl+Click (Cmd on Mac) to send the corresponding *Note off*. + - Test note button. Click to send a Middle C *Note on* MIDI message (Note 60, velocity 64), and Ctrl+Click (Cmd on Mac) to send the corresponding *Note off*. You'll see the little circle next to the button light up when a note is on, either through the Test note button, or when playing the device. + This indicator is not completely reliable, but can be useful to get a sense of MIDI activity. - The row of indicators show different parts of the state of the module/device connection and initialization process. When everything is conencted and working properly, you should see a row of blue indicators. When an indicator is orange, it means the corresponding item is in-progress. - If the module gets stuck, an indicator may remain orange, and you may see the MIDI communication lights stop moving. - If you see this happen you may need to reboot the module from the menu, or unplug/power cycle your device. + If the module gets stuck, an indicator may remain orange and you may see the MIDI communication lights stop moving. + If you see this happen you may need to reset MIDI I/O, reboot the module from the menu, or unplug the Midi or USB or even power cycle your EM device. These indicators are: - 1. Note (a note is currently playing). This indicator is not completely reliable when there is a high level of MIDI traffic, but can be useful to get a sense of activity. 1. MIDI output device connection 1. MIDI input device connection 1. Connected device is a recognized Eagan Matrix, based on the device name 1. System presets initialized 1. User presets initialized 1. Current preset initialized - 1. Saved preset from previous session + 1. Saved preset from previous session is set 1. HC-1 has requested the device for updates when presets change 1. Device heartbeat status (heartbeat can be disabled int he menu). - In the middle is the pachde (#d) logo - - The name of the currently connected EM device + - The name of the currently connected EM device, if any - The device firmware version. ## Module Menu diff --git a/plugin.json b/plugin.json index 2c0d6c9..1be145a 100644 --- a/plugin.json +++ b/plugin.json @@ -22,13 +22,13 @@ { "slug": "pachde-hc-2", "name": "HC-2", - "description": "More Eagan Matrix controls (HC-1 expander)", + "description": "More Eagan Matrix controls (HC-1 companion)", "tags": [ "Expander" ] }, { "slug": "pachde-hc-3", "name": "HC-3", - "description": "Eagan Matrix Set lists (favorites groups)", + "description": "Eagan Matrix set lists (HC-1 companion)", "tags": [ "Expander" ] } ] diff --git a/res/HC-3.svg b/res/HC-3.svg index 5e39801..4b96d40 100644 --- a/res/HC-3.svg +++ b/res/HC-3.svg @@ -4,28 +4,10 @@ xmlns:svg="http://www.w3.org/2000/svg"> - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/HC-1/HC-1-draw.cpp b/src/HC-1/HC-1-draw.cpp index 193196b..238ee85 100644 --- a/src/HC-1/HC-1-draw.cpp +++ b/src/HC-1/HC-1-draw.cpp @@ -68,54 +68,11 @@ void Hc1ModuleWidget::drawStatusDots(NVGcontext* vg) float spacing = 6.25f; float y = box.size.y - 7.5f; - if (my_module) { + if (!my_module) { // note - Dot(vg, left, y, my_module->notesOn ? purple_light : gray_light, my_module->notesOn); - left += spacing; - //device_output_state - Dot(vg, left, y, InitStateColor(my_module->device_output_state)); - left += spacing; - // device_input_state - Dot(vg, left, y, InitStateColor(my_module->device_input_state)); - left += spacing; - //eagan matrix - Dot(vg, left, y, my_module->is_eagan_matrix ? blue_light : yellow_light); - left += spacing; - // //device_state - // Dot(vg, left, y, InitStateColor(my_module->device_hello_state)); - // left += spacing; - //system_preset_state - Dot(vg, left, y, InitStateColor(my_module->system_preset_state)); - left += spacing; - //user_preset_state - Dot(vg, left, y, InitStateColor(my_module->user_preset_state)); - left += spacing; - //apply_favorite_state - Dot(vg, left, y, InitStateColor(my_module->apply_favorite_state)); - left += spacing; - //config_state - Dot(vg, left, y, InitStateColor(my_module->config_state)); - left += spacing; - //saved_preset_state - if (my_module->restore_saved_preset){ - Dot(vg, left, y, InitStateColor(my_module->saved_preset_state)); - } - left += spacing; - //request_updates_state - Dot(vg, left, y, InitStateColor(my_module->request_updates_state)); - left += spacing; - //handshake - if (my_module->heart_beating) { - Dot(vg, left, y, InitStateColor(my_module->handshake)); - } - left += spacing; - // note (debugging) - // auto n = format_string("%d", my_module->note); - // nvgText(vg, left + 5.f, box.size.y - 1.5f, n.c_str(), nullptr); - } else { + Dot(vg, 41.f, y, gray_light, false); + auto co = InitStateColor(InitState::Complete); - Dot(vg, left, y, gray_light, false); - left += spacing; //device_output_state Dot(vg, left, y, co); left += spacing; @@ -125,9 +82,6 @@ void Hc1ModuleWidget::drawStatusDots(NVGcontext* vg) //eagan matrix Dot(vg, left, y, blue_light); left += spacing; - // //device_state - // Dot(vg, left, y, InitStateColor(my_module->device_hello_state)); - // left += spacing; //system_preset_state Dot(vg, left, y, co); left += spacing; @@ -161,6 +115,10 @@ void Hc1ModuleWidget::drawLayer(const DrawArgs& args, int layer) std::string text; SetTextStyle(vg, font_normal, RampGray(G_85), 12.f); if (my_module) { + if (my_module->dupe) { + SetTextStyle(vg, bold_font, GetStockColor(StockColor::Fuchsia), 16.f); + text = "[Only one HC-1 per EM]"; + } else if (my_module->broken) { SetTextStyle(vg, bold_font, GetStockColor(StockColor::Fuchsia), 16.f); text = "[MIDI error - please wait]"; @@ -171,6 +129,9 @@ void Hc1ModuleWidget::drawLayer(const DrawArgs& args, int layer) if (InitState::Uninitialized == my_module->device_input_state) { text = ".. looking for EM input ..."; } else + if (InitState::Uninitialized == my_module->duplicate_instance) { + text = ".. checking for duplicate HC-1 ..."; + } else if (my_module->is_gathering_presets()) { text = format_string("... gathering %s preset %d ...", my_module->in_user_names ? "User" : "System", my_module->in_user_names ? my_module->user_presets.size() : my_module->system_presets.size()); } else @@ -257,38 +218,18 @@ void Hc1ModuleWidget::drawPedals(NVGcontext* vg, std::shared_ptrexpanders.any()) { - bool right_expander = my_module->expanders.right(); - float left, right; - if (right_expander) { - left = box.size.x - 5.5f; - right = box.size.x; - } else { - left = 0.f; - right = 5.5f; - } - float y = 80.f; - Line(vg, left, y, right, y, COLOR_BRAND, 1.75f); - Circle(vg, left, y, 2.5f, COLOR_BRAND); - } -} - void Hc1ModuleWidget::draw(const DrawArgs& args) { ModuleWidget::draw(args); auto vg = args.vg; - bool stock = !my_module || !my_module->ready(); + bool stock = !module || !my_module->ready(); auto rt = stock ? EM_Recirculator::Reverb : my_module->recirculatorType(); - drawExpanderConnector(vg); - auto font = GetPluginFontRegular(); if (FontOk(font)) { #if defined SHOW_PRESET0 - if (my_module) + if (module) { SetTextStyle(vg, font, orange_light, 9.f); auto text = my_module->preset0.describe_short(); @@ -310,8 +251,12 @@ void Hc1ModuleWidget::draw(const DrawArgs& args) drawPedals(vg, font, stock); } + if (module && my_module->dupe) { + BoxRect(vg, 1.f, 1.f, box.size.x-2, box.size.y-2, GetStockColor(StockColor::Orange_red), 1.5f); + } + drawStatusDots(vg); - if (!my_module) { + if (!module) { DrawLogo(args.vg, box.size.x*.5f - 120, 30.f, Overlay(COLOR_BRAND), 14.0); } DrawLogo(vg, box.size.x /2.f - 12.f, RACK_GRID_HEIGHT - ONE_HP, RampGray(G_90)); diff --git a/src/HC-1/HC-1-midi.cpp b/src/HC-1/HC-1-midi.cpp index a7de3b3..485adc0 100644 --- a/src/HC-1/HC-1-midi.cpp +++ b/src/HC-1/HC-1-midi.cpp @@ -1,21 +1,72 @@ #include "HC-1.hpp" #include "../cc_param.hpp" +#include "../HcOne.hpp" namespace pachde { +void Hc1Module::setMidiDevice(int id) +{ + if (-1 == id) { + resetMidiIO(); + device_name = "(no Eagan Matrix device)"; + return; + } + output_device_id = id; + midi_output.setDeviceId(id); + device_name = midi_output.getDeviceName(id); + int id_in = findMatchingInputDevice(device_name); + midi::Input::setDeviceId(id_in); + input_device_id = id_in; + device_output_state = id == -1 ? InitState::Uninitialized : InitState::Complete; + device_input_state = id_in == -1 ? InitState::Uninitialized : InitState::Complete; + notifyDeviceChanged(); + checkDuplicate(); +} + void Hc1Module::resetMidiIO() { + device_name.clear(); + midi_dispatch.clear(); + midi::Input::reset(); input_device_id = -1; device_input_state = InitState::Uninitialized; + midi_output.reset(); output_device_id = -1; device_output_state = InitState::Uninitialized; + + dupe = false; + duplicate_instance = InitState::Uninitialized; + + notifyDeviceChanged(); +} + +void Hc1Module::checkDuplicate() +{ + dupe = false; + auto one = HcOne::get(); + if (one->Hc1count() > 1) { + one->scan([=](Hc1Module* const& m) { + if (m != this + && this->output_device_id >= 0 + && (this->output_device_id == m->output_device_id)) { + dupe = true; + return false; + } + return true; + }); + } + duplicate_instance = InitState::Complete; } void Hc1Module::queueMidiMessage(uMidiMessage msg) { + if (dupe) { + DebugLog("MIDI Output disabled when duplicate"); + return; + } if (midi_dispatch.full()) { DebugLog("MIDI Output queue full: resetting MIDI IO"); resetMidiIO(); diff --git a/src/HC-1/HC-1-presets.cpp b/src/HC-1/HC-1-presets.cpp index 8c216c4..ad7db34 100644 --- a/src/HC-1/HC-1-presets.cpp +++ b/src/HC-1/HC-1-presets.cpp @@ -1,6 +1,32 @@ #include "HC-1.hpp" namespace pachde { +void Hc1Module::tryCachedPresets() { + if (cache_presets) { + loadSystemPresets(); + if (system_presets.empty()) { + system_preset_state = InitState::Uninitialized; + } + loadUserPresets(); + if (user_presets.empty()) { + user_preset_state = InitState::Uninitialized; + } + if (favoritesFile.empty()) { + favoritesFromPresets(); + } + } + if (!favoritesFile.empty()) { + if (system_presets.empty() || user_presets.empty()) { + apply_favorite_state = InitState::Uninitialized; + } else { + if (readFavoritesFile(favoritesFile, true)) { + apply_favorite_state = InitState::Complete; + } else { + apply_favorite_state = InitState::Broken; + } + } + } +} void Hc1Module::setPresetOrder(PresetOrder order) { diff --git a/src/HC-1/HC-1-process.cpp b/src/HC-1/HC-1-process.cpp index 57a84bb..f6a793b 100644 --- a/src/HC-1/HC-1-process.cpp +++ b/src/HC-1/HC-1-process.cpp @@ -1,6 +1,6 @@ #include "HC-1.hpp" #include "../cc_param.hpp" - +#include "../HcOne.hpp" namespace pachde { void Hc1Module::processCV(int paramId) @@ -119,7 +119,7 @@ void Hc1Module::dispatchMidi() void Hc1Module::process(const ProcessArgs& args) { - bool is_ready = ready(); + bool is_ready = ready() && !dupe; if (++check_cv > CV_INTERVAL) { check_cv = 0; @@ -178,7 +178,7 @@ void Hc1Module::process(const ProcessArgs& args) heart_phase += args.sampleTime; if (heart_phase >= heart_time) { heart_phase -= heart_time; - heart_time = 2.5f; + heart_time = 2.1f; // handle device changes if (InitState::Complete == device_output_state @@ -194,6 +194,7 @@ void Hc1Module::process(const ProcessArgs& args) midi_output.setDeviceId(id); midi_output.setChannel(-1); output_device_id = id; + notifyDeviceChanged(); return; } } @@ -210,6 +211,7 @@ void Hc1Module::process(const ProcessArgs& args) midi::Input::setDeviceId(id); midi::Input::setChannel(-1); input_device_id = id; + notifyDeviceChanged(); return; } } @@ -229,9 +231,11 @@ void Hc1Module::process(const ProcessArgs& args) midi_output.setChannel(-1); device_output_state = InitState::Complete; heart_time = 5.f; + notifyDeviceChanged(); return; } else { device_name.clear(); + notifyDeviceChanged(); } } // scan for EM @@ -242,8 +246,13 @@ void Hc1Module::process(const ProcessArgs& args) midi_output.setChannel(-1); device_output_state = InitState::Complete; heart_time =5.f; + break; } } + if (!is_EMDevice(device_name)) { + device_name = ""; + } + notifyDeviceChanged(); return; } break; case InitState::Complete: break; @@ -276,8 +285,18 @@ void Hc1Module::process(const ProcessArgs& args) && (notesOn <= 0) && !in_preset ) { + if (InitState::Uninitialized == duplicate_instance) { + checkDuplicate(); + } + if (dupe) return; + if (InitState::Uninitialized == system_preset_state || InitState::Broken == system_preset_state) { - transmitRequestSystemPresets(); + if (InitState::Broken != system_preset_state) { + tryCachedPresets(); + } + if (system_preset_state != InitState::Complete) { + transmitRequestSystemPresets(); + } } else if (InitState::Uninitialized == user_preset_state || InitState::Broken == user_preset_state) { transmitRequestUserPresets(); diff --git a/src/HC-1/HC-1-ui.cpp b/src/HC-1/HC-1-ui.cpp index caa3db4..2352c02 100644 --- a/src/HC-1/HC-1-ui.cpp +++ b/src/HC-1/HC-1-ui.cpp @@ -2,10 +2,10 @@ #include "HC-1-layout.hpp" #include "../cc_param.hpp" #include "../components.hpp" +#include "../indicator_widget.hpp" #include "../misc.hpp" #include "../port.hpp" #include "../small_push.hpp" - namespace pachde { Hc1ModuleWidget::Hc1ModuleWidget(Hc1Module* module) @@ -16,9 +16,18 @@ Hc1ModuleWidget::Hc1ModuleWidget(Hc1Module* module) if (my_module) { tab = my_module->tab; page = my_module->getTabPage(tab); - my_module->ui_event_sink = this; } createUi(); + if (my_module) { + my_module->subscribeHcEvents(this); + } +} + +Hc1ModuleWidget::~Hc1ModuleWidget() +{ + if (module) { + my_module->unsubscribeHcEvents(this); + } } const std::string Hc1ModuleWidget::macroName(int m) @@ -235,37 +244,22 @@ void Hc1ModuleWidget::createTranspose() void Hc1ModuleWidget::createMidiSelection() { - auto pm = createWidget(Vec(7.5f, box.size.y - 16.f)); - pm->describe("Midi input"); - if (my_module) { - pm->setMidiPort(my_module); - } - addChild(pm); - - pm = createWidget(Vec(20.f, box.size.y - 16.f)); - pm->describe("Midi output"); - if (my_module) { - pm->setMidiPort(&my_module->midi_output); + em_picker = createWidget(Vec(7.5f, box.size.y - 16.f)); + em_picker->describe("Choose Eagan Matrix device"); + if (module) { + em_picker->setExternals(&my_module->midi_output, my_module); } - addChild(pm); + addChild(em_picker); } void Hc1ModuleWidget::createDeviceDisplay() { // device name - // todo: set text only when device name changes - addChild(createDynamicLabel( - Vec(box.size.x*.5f + 25.f, box.size.y - 14.f), 100.f, - [=]() { - std::string device_name; - device_name = my_module ? my_module->deviceName() : ""; - if (device_name.empty()) { - device_name = "(no Eagan Matrix available)"; - } - return device_name; - }, + device_label = createStaticTextLabel( + Vec(box.size.x*.5f + 25.f, box.size.y - 14.f), 100.f, "", TextAlignment::Left, 12.f, false - )); + ); + addChild(device_label); // firmare version // todo: set text only when firmware version changes @@ -281,7 +275,7 @@ void Hc1ModuleWidget::createDeviceDisplay() void Hc1ModuleWidget::createTestNote() { - auto pb = createWidgetCentered(Vec(45.f, box.size.y -8.5f)); + auto pb = createWidgetCentered(Vec(32.f, box.size.y -8.5f)); if (my_module) { #ifdef ARCH_MAC pb->describe("Send test Note\nCmd+click = Note off"); @@ -299,6 +293,44 @@ void Hc1ModuleWidget::createTestNote() addChild(pb); } +void Hc1ModuleWidget::createStatusDots() +{ + float left = 60.f; + float spacing = 6.25f; + float y = box.size.y - 7.5f; + + if (my_module) { + addChild(createIndicatorCentered(41.f, y, "Note", [=]()->const NVGcolor& { return (my_module->notesOn ? purple_light : gray_light); }, [=]()-> bool { return my_module->notesOn; })); + //device_output_state + addChild(createIndicatorCentered(left, y, "Midi output device", [=]()->const NVGcolor& { return InitStateColor(my_module->device_output_state); })); + left += spacing; + // device_input_state + addChild(createIndicatorCentered(left, y, "Midi input device", [=]()->const NVGcolor& { return InitStateColor(my_module->device_input_state); })); + left += spacing; + //eagan matrix + addChild(createIndicatorCentered(left, y, "Eagan Matrix", [=]()->const NVGcolor& { return my_module->is_eagan_matrix ? blue_light : yellow_light; })); + left += spacing; + //system_preset_state + addChild(createIndicatorCentered(left, y, "System presets", [=]()->const NVGcolor& { return InitStateColor(my_module->system_preset_state); })); + left += spacing; + //user_preset_state + addChild(createIndicatorCentered(left, y, "User presets", [=]()->const NVGcolor& { return InitStateColor(my_module->user_preset_state); })); + left += spacing; + //apply_favorite_state + addChild(createIndicatorCentered(left, y, "Favorites applied", [=]()->const NVGcolor& { return InitStateColor(my_module->apply_favorite_state); })); + left += spacing; + //config_state + addChild(createIndicatorCentered(left, y, "Received configuration", [=]()->const NVGcolor& { return InitStateColor(my_module->config_state); })); + left += spacing; + //saved_preset_state + addChild(createIndicatorCentered(left, y, "Set saved preset", [=]()->const NVGcolor& { return InitStateColor(my_module->saved_preset_state); })); + left += spacing; + addChild(createIndicatorCentered(left, y, "Request updates", [=]()->const NVGcolor& { return InitStateColor(my_module->request_updates_state); })); + left += spacing; + addChild(createIndicatorCentered(left, y, "Heartbeat", [=]()->const NVGcolor& { return InitStateColor(my_module->handshake); })); + } +} + void Hc1ModuleWidget::createUi() { status_light = createLightCentered>(Vec(12.f, 12.f), my_module, Hc1Module::HEART_LIGHT); @@ -382,6 +414,7 @@ void Hc1ModuleWidget::createUi() #endif createMidiSelection(); createTestNote(); + createStatusDots(); createDeviceDisplay(); } @@ -408,11 +441,58 @@ void Hc1ModuleWidget::pageDown() page_down->enable(static_cast(page*24) < max-24); } +// +// IPresetHolder +// +void Hc1ModuleWidget::setPreset(std::shared_ptr preset) +{ + if (! my_module) return; + my_module->setPreset(preset); +} +bool Hc1ModuleWidget::isCurrentPreset(std::shared_ptr preset) +{ + return my_module ? my_module->isCurrentPreset(preset) : false; +} +void Hc1ModuleWidget::addFavorite(std::shared_ptr preset) +{ + if (! my_module) return; + my_module->addFavorite(preset); + updatePresetWidgets(); +} +void Hc1ModuleWidget::unFavorite(std::shared_ptr preset) +{ + if (! my_module) return; + my_module->unFavorite(preset); + updatePresetWidgets(); +} +void Hc1ModuleWidget::moveFavorite(std::shared_ptr preset, FavoriteMove motion) +{ + if (! my_module) return; + my_module->moveFavorite(preset, motion); + updatePresetWidgets(); +} + +// +// IHandleHcEvents +// void Hc1ModuleWidget::onPresetChanged(const PresetChangedEvent& e) { favorite->setPreset(my_module->current_preset); showCurrentPreset(true); } +void Hc1ModuleWidget::onDeviceChanged(const DeviceChangedEvent& e) +{ + em_picker->describe(e.name.empty() + ? "Choose Eagan Matrix device" + : format_string("EM device: %s", e.name.c_str())); + + device_label->text(e.name.empty() ? "" : e.name); +} + +void Hc1ModuleWidget::onDisconnect(const DisconnectEvent& e) +{ + device_label->text(""); +} void Hc1ModuleWidget::toCategory(uint16_t code) { diff --git a/src/HC-1/HC-1.cpp b/src/HC-1/HC-1.cpp index 5683e63..1e32228 100644 --- a/src/HC-1/HC-1.cpp +++ b/src/HC-1/HC-1.cpp @@ -1,15 +1,14 @@ #include "HC-1.hpp" #include "../cc_param.hpp" -#include "../misc.hpp" #include "../HcOne.hpp" +#include "../misc.hpp" namespace pachde { Hc1Module::Hc1Module() { - HcOne::get()->registerHc1(this); - //system_presets.reserve(700); - //user_presets.reserve(128); + system_presets.reserve(700); + user_presets.reserve(128); config(Params::NUM_PARAMS, Inputs::NUM_INPUTS, Outputs::NUM_OUTPUTS, Lights::NUM_LIGHTS); configCCParam(em_midi::EMCC_i, true, this, M1_PARAM, M1_INPUT, M1_REL_PARAM, M1_REL_LIGHT, 0.f, EM_Max14f, EM_Max14f/2.f, "i"); configCCParam(em_midi::EMCC_ii, true, this, M2_PARAM, M2_INPUT, M2_REL_PARAM, M2_REL_LIGHT, 0.f, EM_Max14f, EM_Max14f/2.f, "ii"); @@ -61,16 +60,79 @@ Hc1Module::Hc1Module() getLight(HEART_LIGHT).setBrightness(.8f); clearCCValues(); + + HcOne::get()->registerHc1(this); } Hc1Module::~Hc1Module() { + notifyDisconnect(); HcOne::get()->unregisterHc1(this); if (restore_ui_data) { delete restore_ui_data; } } +// +// IHandleHcEvents notification +// +void Hc1Module::subscribeHcEvents(IHandleHcEvents*client) +{ + if (event_subscriptions.empty() + || event_subscriptions.cend() == std::find(event_subscriptions.cbegin(), event_subscriptions.cend(), client)) + { + event_subscriptions.push_back(client); + + auto dce = IHandleHcEvents::DeviceChangedEvent{device_name}; + client->onDeviceChanged(dce); + + auto pce = IHandleHcEvents::PresetChangedEvent{current_preset}; + client->onPresetChanged(pce); + + auto rce = IHandleHcEvents::RoundingChangedEvent{rounding}; + client->onRoundingChanged(rce); + } +} +void Hc1Module::unsubscribeHcEvents(IHandleHcEvents*client) +{ + auto it = std::find(event_subscriptions.begin(), event_subscriptions.end(), client); + if (it != event_subscriptions.end()) { + event_subscriptions.erase(it); + } +} +void Hc1Module::notifyPresetChanged() +{ + if (event_subscriptions.empty()) return; + auto event = IHandleHcEvents::PresetChangedEvent{current_preset}; + for (auto client: event_subscriptions) { + client->onPresetChanged(event); + } +} +void Hc1Module::notifyRoundingChanged() +{ + if (event_subscriptions.empty()) return; + auto event = IHandleHcEvents::RoundingChangedEvent{rounding}; + for (auto client: event_subscriptions) { + client->onRoundingChanged(event); + } +} +void Hc1Module::notifyDeviceChanged() +{ + if (event_subscriptions.empty()) return; + auto event = IHandleHcEvents::DeviceChangedEvent{device_name}; + for (auto client: event_subscriptions) { + client->onDeviceChanged(event); + } +} +void Hc1Module::notifyDisconnect() +{ + if (event_subscriptions.empty()) return; + auto event = IHandleHcEvents::DisconnectEvent{}; + for (auto client: event_subscriptions) { + client->onDisconnect(event); + } +} + void Hc1Module::centerKnobs() { paramToDefault(M1_PARAM); paramToDefault(M2_PARAM); @@ -141,6 +203,7 @@ void Hc1Module::onSave(const SaveEvent& e) void Hc1Module::onRemove(const RemoveEvent& e) { + notifyDisconnect(); HcOne::get()->unregisterHc1(this); savePresets(); Module::onRemove(e); @@ -214,24 +277,12 @@ void Hc1Module::dataFromJson(json_t *root) favoritesFile = json_string_value(j); } cache_presets = GetBool(root, "cache-presets", cache_presets); - if (cache_presets) { - loadSystemPresets(); - loadUserPresets(); - if (favoritesFile.empty()) { - favoritesFromPresets(); - } - } - if (!system_presets.empty() && !favoritesFile.empty()) { - if (readFavoritesFile(favoritesFile, true)) { - apply_favorite_state = InitState::Complete; - } else { - apply_favorite_state = InitState::Broken; - } - } + tryCachedPresets(); } void Hc1Module::reboot() { + dupe = false; device_name = ""; midi::Input::reset(); midi_output.reset(); @@ -250,6 +301,7 @@ void Hc1Module::reboot() device_output_state = device_input_state = system_preset_state = + duplicate_instance = user_preset_state = apply_favorite_state = config_state = @@ -265,6 +317,8 @@ void Hc1Module::reboot() data_stream = -1; //download_message_id = -1; recirculator = 0; + + notifyDeviceChanged(); } void Hc1Module::onRandomize(const RandomizeEvent& e) diff --git a/src/HC-1/HC-1.hpp b/src/HC-1/HC-1.hpp index 06ff9c5..889414c 100644 --- a/src/HC-1/HC-1.hpp +++ b/src/HC-1/HC-1.hpp @@ -4,6 +4,7 @@ #include #include "../colors.hpp" #include "../em_midi.hpp" +#include "../em_picker.hpp" #include "../em_types.hpp" #include "../favorite_widget.hpp" #include "../hc_events.hpp" @@ -22,7 +23,7 @@ namespace pachde { #include "../debug_log.hpp" const NVGcolor& StatusColor(StatusItem led); -struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module +struct Hc1Module : IPresetHolder, ISendMidi, ISetDevice, midi::Input, Module { enum Params { @@ -66,7 +67,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module std::vector> system_presets; std::vector> favorite_presets; - IHandleHcEvents * ui_event_sink = nullptr; + std::vector event_subscriptions; // ui persistence PresetTab tab = PresetTab::User; @@ -139,9 +140,9 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module } bool is_eagan_matrix = false; - InitState device_output_state = InitState::Uninitialized; InitState device_input_state = InitState::Uninitialized; + InitState duplicate_instance = InitState::Uninitialized; InitState system_preset_state = InitState::Uninitialized; InitState user_preset_state = InitState::Uninitialized; InitState apply_favorite_state = InitState::Uninitialized; @@ -153,7 +154,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module bool hasSystemPresets() { return InitState::Complete == system_preset_state && !system_presets.empty(); } bool hasUserPresets() { return InitState::Complete == user_preset_state && !user_presets.empty(); } bool hasConfig() { return InitState::Complete == config_state; } - + void tryCachedPresets(); bool configPending() { return InitState::Pending == config_state; } bool savedPresetPending() { return InitState::Pending == saved_preset_state; } bool handshakePending() { return InitState::Pending == handshake; } @@ -172,6 +173,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module return !broken && InitState::Complete == device_output_state && InitState::Complete == device_input_state + && InitState::Complete == duplicate_instance && InitState::Complete == system_preset_state && InitState::Complete == user_preset_state && InitState::Complete == apply_favorite_state @@ -185,6 +187,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module bool in_user_names = false; bool in_sys_names = false; bool broken = false; + bool dupe = false; #ifdef VERBOSE_LOG bool log_midi = false; #endif @@ -249,9 +252,19 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module bool is_gathering_presets() { return system_preset_state == InitState::Pending || user_preset_state == InitState::Pending; } Hc1Module(); - virtual ~Hc1Module(); + // ISetDevice + void setMidiDevice(int id) override; + + // IHandleHcEvents subscription and notification + void subscribeHcEvents(IHandleHcEvents* client); + void unsubscribeHcEvents(IHandleHcEvents* client); + void notifyPresetChanged(); + void notifyRoundingChanged(); + void notifyDeviceChanged(); + void notifyDisconnect(); + // midi::Input void onMessage(const midi::Message& msg) override; @@ -319,69 +332,16 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module void addFavorite(std::shared_ptr preset) override; void unFavorite(std::shared_ptr preset) override; void moveFavorite(std::shared_ptr preset, FavoriteMove motion) override; + void numberFavorites(); + void sortFavorites(PresetOrder order = PresetOrder::Favorite); void sendSavedPreset(); std::shared_ptr findDefinedPreset(std::shared_ptr preset); - void notifyPresetChanged(rack::engine::Module::Expander& expander, const IHandleHcEvents::PresetChangedEvent& e) - { - if (!expander.module) return; - auto sink = dynamic_cast(expander.module); - if (!sink) return; - sink->onPresetChanged(e); - } - void notifyPresetChanged() - { - auto event = IHandleHcEvents::PresetChangedEvent{current_preset}; - if (ui_event_sink) { - ui_event_sink->onPresetChanged(event); - } - notifyPresetChanged(getRightExpander(), event); - notifyPresetChanged(getLeftExpander(), event); - } - - void notifyRoundingChanged(rack::engine::Module::Expander& expander, IHandleHcEvents::RoundingChangedEvent e) - { - if (!expander.module) return; - auto sink = dynamic_cast(expander.module); - if (!sink) return; - sink->onRoundingChanged(e); - } - void notifyRoundingChanged() - { - auto event = IHandleHcEvents::RoundingChangedEvent{rounding}; - if (ui_event_sink) { - ui_event_sink->onRoundingChanged(event); - } - notifyRoundingChanged(getRightExpander(), event); - notifyRoundingChanged(getLeftExpander(), event); - } - - void numberFavorites(); - void sortFavorites(PresetOrder order = PresetOrder::Favorite); - - // expanders - ExpanderPresence expanders = Expansion::None; - // Only handles removal. We depend on adjacent modules to tell us - // that they are an expander via expanderAdded(). - void onExpanderChange(const ExpanderChangeEvent& e) override - { - if (e.side) { - if (!getRightExpander().module) { - expanders.removeRight(); - } - } else { - if (!getLeftExpander().module) { - expanders.removeLeft(); - } - } - } - void expanderAdded(Expansion side) { - expanders.add(side); - } void syncStatusLights(); void syncParam(int paramId); void syncParams(float sampleTime); + void checkDuplicate(); void resetMidiIO(); void sendResetAllreceivers(); void transmitRequestUpdates(); @@ -407,6 +367,7 @@ struct Hc1Module : IPresetHolder, ISendMidi, midi::Input, Module }; // ----------------------------------------------------------------------------------------- +const NVGcolor& InitStateColor(InitState state); using Hc1p = Hc1Module::Params; using Hc1in = Hc1Module::Inputs; using Hc1out = Hc1Module::Outputs; @@ -415,18 +376,20 @@ using Hc1lt = Hc1Module::Lights; struct Hc1ModuleWidget : ModuleWidget, IPresetHolder, IHandleHcEvents { Hc1Module* my_module = nullptr; - + StaticTextLabel* device_label = nullptr; std::vector presets; bool have_preset_widgets = false; - TabBarWidget* tab_bar; - FavoriteWidget* favorite; + TabBarWidget* tab_bar = nullptr; + FavoriteWidget* favorite = nullptr; PresetTab tab = PresetTab::Favorite; int page = 0; - SquareButton* page_up; - SquareButton* page_down; - GrayModuleLightWidget * status_light; + SquareButton* page_up = nullptr; + SquareButton* page_down = nullptr; + GrayModuleLightWidget * status_light = nullptr; + EMPicker* em_picker = nullptr; explicit Hc1ModuleWidget(Hc1Module *module); + virtual ~Hc1ModuleWidget(); // HC-1-ui.cpp: make ui void createPresetGrid(); @@ -439,6 +402,7 @@ struct Hc1ModuleWidget : ModuleWidget, IPresetHolder, IHandleHcEvents void createMidiSelection(); void createDeviceDisplay(); void createTestNote(); + void createStatusDots(); void createUi(); void setTab(PresetTab tab, bool force = false); @@ -453,40 +417,19 @@ struct Hc1ModuleWidget : ModuleWidget, IPresetHolder, IHandleHcEvents bool showCurrentPreset(bool changeTab); // IPresetHolder - void setPreset(std::shared_ptr preset) override - { - if (! my_module) return; - my_module->setPreset(preset); - } - bool isCurrentPreset(std::shared_ptr preset) override - { - return my_module ? my_module->isCurrentPreset(preset) : false; - } - void addFavorite(std::shared_ptr preset) override - { - if (! my_module) return; - my_module->addFavorite(preset); - updatePresetWidgets(); - } - void unFavorite(std::shared_ptr preset) override - { - if (! my_module) return; - my_module->unFavorite(preset); - updatePresetWidgets(); - } - void moveFavorite(std::shared_ptr preset, FavoriteMove motion) override - { - if (! my_module) return; - my_module->moveFavorite(preset, motion); - updatePresetWidgets(); - } + void setPreset(std::shared_ptr preset) override; + bool isCurrentPreset(std::shared_ptr preset) override; + void addFavorite(std::shared_ptr preset) override; + void unFavorite(std::shared_ptr preset) override; + void moveFavorite(std::shared_ptr preset, FavoriteMove motion) override; // IHandleHcEvents void onPresetChanged(const PresetChangedEvent& e) override; void onRoundingChanged(const RoundingChangedEvent& e) override {} + void onDeviceChanged(const DeviceChangedEvent& e) override; + void onDisconnect(const DisconnectEvent& e) override; // HC-1.draw.cpp - void drawExpanderConnector(NVGcontext* vg); void drawDSP(NVGcontext* vg); void drawStatusDots(NVGcontext* vg); void drawPedals(NVGcontext* vg, std::shared_ptr font, bool stockPedals); diff --git a/src/HC-2/HC-2-ui.cpp b/src/HC-2/HC-2-ui.cpp index 8cd3d55..8f4837f 100644 --- a/src/HC-2/HC-2-ui.cpp +++ b/src/HC-2/HC-2-ui.cpp @@ -38,6 +38,7 @@ inline uint8_t GetSmallParamValue(rack::app::ModuleWidget* w, int id, uint8_t de if (!pq) return default_value; return U8(pq->getValue()); } + void Hc2ModuleWidget::createRoundingUI(float x, float y) { // Rounding cluster @@ -95,24 +96,16 @@ Hc2ModuleWidget::Hc2ModuleWidget(Hc2Module * module) } setPanel(createPanel(asset::plugin(pluginInstance, "res/HC-2.svg"))); - // current preset in title - preset_label = createStaticTextLabel( - Vec(70.f, 7.5f), 250.f, "My Amazing Preset", TextAlignment::Left, 14.f, true, preset_name_color); - preset_label->bright(); - addChild(preset_label); - - createRoundingUI(15.f, 40.f); - - // device name + // device name in title device_label = createStaticTextLabel( - Vec(box.size.x*.5f + 25.f, box.size.y - 14.f), 100.f, - "", TextAlignment::Left, 12.f, false ); + Vec(62.f, 9.), 150.f, "", TextAlignment::Left, 12.f, false ); addChild(device_label); + + createRoundingUI(15.f, 40.f); } void Hc2ModuleWidget::onPresetChanged(const PresetChangedEvent& e) { - preset_label->text(e.preset ? e.preset->name : ""); rounding_summary->modified(); } @@ -121,32 +114,20 @@ void Hc2ModuleWidget::onRoundingChanged(const RoundingChangedEvent& e) rounding_summary->modified(); } -void Hc2ModuleWidget::step() +void Hc2ModuleWidget::onDeviceChanged(const DeviceChangedEvent& e) { - ModuleWidget::step(); - - if (device_label) { - std::string device = my_module ? my_module->getDeviceName() : ""; - if (device_label->getText() != device) { - device_label->text(device); - } - } + device_label->text(e.name); } -void Hc2ModuleWidget::drawExpanderConnector(const DrawArgs& args) +void Hc2ModuleWidget::onDisconnect(const DisconnectEvent& e) { - if (!my_module || my_module->partner_side.empty()) return; - auto vg = args.vg; + device_label->text(""); +} - auto right = my_module->partner_side.right(); - float cy = 80.f; - if (right) { - Line(vg, box.size.x - 5.5f, cy, box.size.x , cy, COLOR_BRAND, 1.75f); - Circle(vg, box.size.x - 5.5f, cy, 2.5f, COLOR_BRAND); - } else { - Line(vg, 0.f, cy, 5.5f, cy, COLOR_BRAND, 1.75f); - Circle(vg, 5.5f, cy, 2.5f, COLOR_BRAND); - } +Hc1Module* Hc2ModuleWidget::getPartner() +{ + if (!my_module) return nullptr; + return my_module->getPartner(); } void drawMap(NVGcontext* vg, uint8_t * map, float x, float y) @@ -180,17 +161,16 @@ void Hc2ModuleWidget::draw(const DrawArgs& args) auto vg = args.vg; BoxRect(vg, 15.f, 40.f, ROUND_BOX_WIDTH, ROUND_BOX_HEIGHT, RampGray(G_40), 0.5f); - auto partner = my_module ? my_module->getPartner() : nullptr; + auto partner = getPartner(); if (partner) { drawCCMap(args, partner); } - drawExpanderConnector(args); DrawLogo(vg, box.size.x /2.f - 12.f, RACK_GRID_HEIGHT - ONE_HP, RampGray(G_90)); } void Hc2ModuleWidget::appendContextMenu(Menu *menu) { - auto partner = my_module ? my_module->getPartner() : nullptr; + auto partner = getPartner(); menu->addChild(new MenuSeparator); if (partner) { menu->addChild(createMenuItem("Clear CC Map", "", diff --git a/src/HC-2/HC-2.cpp b/src/HC-2/HC-2.cpp index a105042..e3728a4 100644 --- a/src/HC-2/HC-2.cpp +++ b/src/HC-2/HC-2.cpp @@ -3,6 +3,7 @@ #include "../cc_param.hpp" #include "../colors.hpp" #include "../components.hpp" +#include "../HcOne.hpp" #include "../misc.hpp" #include "../text.hpp" @@ -30,49 +31,30 @@ Hc2Module::Hc2Module() }); configTuningParam(this, P_ROUND_TUNING); - configParam(P_TEST, 0.f, 1.f, .5f, "Test"); -} + //configParam(P_TEST, 0.f, 1.f, .5f, "Test"); + -Hc1Module* Hc2Module::getPartner() -{ - if (partner_side.left()) { - return dynamic_cast(getLeftExpander().module); - } - if (partner_side.right()) { - return dynamic_cast(getRightExpander().module); - } - return nullptr; } -std::string Hc2Module::getDeviceName() +Hc2Module::~Hc2Module() { - if (auto partner = getPartner()) { - return partner->device_name; + auto partner = getPartner(); + if (partner){ + partner->unsubscribeHcEvents(this); } - return ""; } -void Hc2Module::onExpanderChange(const ExpanderChangeEvent& e) +Hc1Module* Hc2Module::getPartner() { - partner_side.clear(); - auto left = dynamic_cast(getLeftExpander().module); - auto right = dynamic_cast(getRightExpander().module); - Hc1Module* partner = nullptr; - if (left) { - partner_side.addLeft(); - left->expanderAdded(Expansion::Right); - partner = left; - } - if (right) { - partner_side.addRight(); - right->expanderAdded(Expansion::Left); - partner = right; - } + auto partner = HcOne::get()->getSoleHc1(); if (partner) { - pullRounding(partner); - } else { - rounding.clear(); + if (!partner_subscribed) { + partner->subscribeHcEvents(this); + partner_subscribed = true; + } + return partner; } + return nullptr; } void Hc2Module::onPresetChanged(const PresetChangedEvent& e) @@ -119,6 +101,20 @@ void Hc2Module::onRoundingChanged(const RoundingChangedEvent& e) } } +void Hc2Module::onDeviceChanged(const DeviceChangedEvent& e) +{ + if (ui_event_sink) { + ui_event_sink->onDeviceChanged(e); + } +} + +void Hc2Module::onDisconnect(const DisconnectEvent& e) +{ + if (ui_event_sink) { + ui_event_sink->onDisconnect(e); + } +} + void Hc2Module::pullRounding(Hc1Module * partner) { if (!partner) partner = getPartner(); diff --git a/src/HC-2/HC-2.hpp b/src/HC-2/HC-2.hpp index 9488352..6d350b8 100644 --- a/src/HC-2/HC-2.hpp +++ b/src/HC-2/HC-2.hpp @@ -27,7 +27,7 @@ struct Hc2Module : Module, ISendMidi, IHandleHcEvents P_ROUND_TUNING, // 0..69 //P_ROUND_EQUAL, P_ROUND_RATE_REL, - P_TEST, + //P_TEST, NUM_PARAMS, }; enum Inputs @@ -49,8 +49,8 @@ struct Hc2Module : Module, ISendMidi, IHandleHcEvents Rounding rounding; IHandleHcEvents * ui_event_sink = nullptr; - ExpanderPresence partner_side = Expansion::None; Hc1Module* getPartner(); + bool partner_subscribed = false; // cv processing const int CV_INTERVAL = 64; @@ -59,11 +59,11 @@ struct Hc2Module : Module, ISendMidi, IHandleHcEvents rack::dsp::SchmittTrigger round_initial_trigger; Hc2Module(); - void onExpanderChange(const ExpanderChangeEvent& e) override; + virtual ~Hc2Module(); + void onSampleRateChange() override { control_rate.onSampleRateChanged(); } - std::string getDeviceName(); // json_t * dataToJson() override; // void dataFromJson(json_t *root) override; @@ -87,12 +87,13 @@ struct Hc2Module : Module, ISendMidi, IHandleHcEvents // IHandleHcEvents void onPresetChanged(const PresetChangedEvent& e) override; void onRoundingChanged(const RoundingChangedEvent& e) override; + void onDeviceChanged(const DeviceChangedEvent& e) override; + void onDisconnect(const DisconnectEvent& e) override; }; struct Hc2ModuleWidget : ModuleWidget, IHandleHcEvents { Hc2Module * my_module = nullptr; - DynamicTextLabel* preset_label = nullptr; StaticTextLabel* device_label = nullptr; DynamicTextLabel* rounding_summary = nullptr; @@ -102,14 +103,15 @@ struct Hc2ModuleWidget : ModuleWidget, IHandleHcEvents my_module->ui_event_sink = nullptr; } } + Hc1Module* getPartner(); void createRoundingUI(float x, float y); // IHandleHcEvents void onPresetChanged(const PresetChangedEvent& e) override; void onRoundingChanged(const RoundingChangedEvent& e) override; + void onDeviceChanged(const DeviceChangedEvent& e) override; + void onDisconnect(const DisconnectEvent& e) override; - void step() override; - void drawExpanderConnector(const DrawArgs& args); void drawCCMap(const DrawArgs& args, Hc1Module * partner); void draw(const DrawArgs& args) override; void appendContextMenu(Menu *menu) override; diff --git a/src/HC-3/HC-3-ui.cpp b/src/HC-3/HC-3-ui.cpp index fd4b843..5b5629e 100644 --- a/src/HC-3/HC-3-ui.cpp +++ b/src/HC-3/HC-3-ui.cpp @@ -1,5 +1,7 @@ #include "hc-3.hpp" #include "../HcOne.hpp" +#include "../misc.hpp" +#include "preset_file_widget.hpp" namespace pachde { @@ -17,6 +19,7 @@ Hc3ModuleWidget::Hc3ModuleWidget(Hc3Module* module) float y = START_ROW; float x = 15.f; for (auto i = 0; i < 16; ++i) { + addChild(createPFWidget(Vec(x - 7.5, y - 7.5), module, i, &drawButton)); addChild(createLightCentered>(Vec(x,y), module, Hc3Module::Lights::SETLIST + i)); y += ITEM_INTERVAL; if (i == 7) { @@ -34,24 +37,6 @@ Hc3ModuleWidget::Hc3ModuleWidget(Hc3Module* module) // { // } -void Hc3ModuleWidget::drawLayer(const DrawArgs& args, int layer) -{ - ModuleWidget::drawLayer(args, layer); - if (layer != 1 - || !my_module - || my_module->loaded_id < 0 - || my_module->files[my_module->loaded_id].empty()) { - return; - } - auto id = my_module->loaded_id; - auto vg = args.vg; - auto font = GetPluginFontSemiBold(); - const float y = START_ROW + LABEL_VOFFSET + (id * ITEM_INTERVAL) + (DIVIDER_OFFSET * (id > 7)); - SetTextStyle(vg, font, preset_name_color, 9.f); - nvgTextAlign(vg, NVG_ALIGN_LEFT); - nvgText(vg, LABEL_COLUMN, y, my_module->files[id].c_str(), nullptr); -} - void Hc3ModuleWidget::draw(const DrawArgs& args) { ModuleWidget::draw(args); @@ -59,25 +44,18 @@ void Hc3ModuleWidget::draw(const DrawArgs& args) auto font = GetPluginFontRegular(); SetTextStyle(vg, font, RampGray(G_85), 9.f); - if (my_module) { + if (module) { auto one = HcOne::get(); auto hc1 = one->getSoleHc1(); std::string info = ""; if (hc1) { info = hc1->deviceName(); + } else if (one->Hc1count() > 1) { + info = ""; } else { - info = format_string("%d", one->Hc1count()); + info = ""; } CenterText(vg, box.size.x*.5f, 30.f, info.c_str(), nullptr); - nvgTextAlign(vg, NVG_ALIGN_LEFT); - - float y = START_ROW + LABEL_VOFFSET; - for (auto n = 0; n < 16; ++n) { - if (!my_module->files[n].empty() && my_module->loaded_id != n) { - nvgText(vg, LABEL_COLUMN, y, my_module->files[n].c_str(), nullptr); - } - y += ITEM_INTERVAL + ((n == 7) * DIVIDER_OFFSET); - } } else { CenterText(vg, box.size.x*.5f, 30.f, "", nullptr); } @@ -87,7 +65,40 @@ void Hc3ModuleWidget::draw(const DrawArgs& args) void Hc3ModuleWidget::appendContextMenu(Menu *menu) { + if (!module) return; + ///pick companion hc1 + + menu->addChild(new MenuSeparator); + + auto count = std::count_if(my_module->files.cbegin(), my_module->files.cend(), [](const std::string& s){ return !s.empty(); }); + bool any = count > 0; + menu->addChild(createMenuItem("Clear", "", [=](){ my_module->clearFiles(); })); + + menu->addChild(createMenuItem("Sort", "", [=](){ + std::sort(my_module->files.begin(), my_module->files.end(), alpha_order); + }, !any)); + menu->addChild(createMenuItem("Compact", "", [=](){ + int gap = 0; + std::vector items; + items.reserve(16); + int n = 0; + for (auto s: my_module->files) { + if (s.empty()) { + ++gap; + } else { + items.push_back(s); + if (n == my_module->loaded_id) { + my_module->loaded_id = n - gap; + } + } + ++n; + } + for (n = 0; n < gap; ++n) { + items.push_back(""); + } + my_module->files = items; + }, !any || 16 == count)); } } \ No newline at end of file diff --git a/src/HC-3/HC-3.cpp b/src/HC-3/HC-3.cpp index dc23a03..9983ae3 100644 --- a/src/HC-3/HC-3.cpp +++ b/src/HC-3/HC-3.cpp @@ -1,39 +1,72 @@ #include "HC-3.hpp" +#include "../HcOne.hpp" namespace pachde { Hc3Module::Hc3Module() { + clearFiles(); config(Params::NUM_PARAMS, Inputs::NUM_INPUTS, Outputs::NUM_OUTPUTS, Lights::NUM_LIGHTS); configSwitch(SELECTED_PARAM, -1.f, 15.f, -1.f, "Current", { "None", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7", "2.8", }); - files[4] = "Hello 4"; - files[5] = "Hello 5"; - files[12] = "Twelve"; } -// note: save in device-specific files in plugin folder not patch settings? -// or save in rack settings in the plugin callbacks? +void Hc3Module::clearFiles() +{ + loaded_id = -1; + files.clear(); + for (auto n = 0; n < 16; ++n) { + files.push_back(""); + } +} + +void Hc3Module::onReset() +{ + clearFiles(); +} + json_t * Hc3Module::dataToJson() { auto root = json_object(); + + auto hc1 = HcOne::get()->getSoleHc1(); + std::string device = hc1 ? hc1->deviceName() : ""; + json_object_set_new(root, "device", json_stringn(device.c_str(), device.size())); + + if (!files.empty()) { + auto jar = json_array(); + for (auto f: files) { + json_array_append_new(jar, json_stringn(f.c_str(), f.size())); + } + json_object_set_new(root, "fav_files", jar); + } return root; } void Hc3Module::dataFromJson(json_t *root) { + auto jar = json_object_get(root, "fav_files"); + if (jar) { + files.clear(); + json_t* jp; + size_t index; + json_array_foreach(jar, index, jp) { + files.push_back(json_string_value(jp)); + } + } } void Hc3Module::process(const ProcessArgs& args) { - if (1 == args.frame % 128) { + if (1 == args.frame % 1024) { for (int n = 0; n < 16; ++n) { if (n == loaded_id) { getLight(n).setBrightness(1.f); + assert(!files[n].empty()); } else { getLight(n).setBrightness(.08f * (!files[n].empty())); } diff --git a/src/HC-3/HC-3.hpp b/src/HC-3/HC-3.hpp index 66ae47e..696da3f 100644 --- a/src/HC-3/HC-3.hpp +++ b/src/HC-3/HC-3.hpp @@ -5,6 +5,7 @@ #include "../plugin.hpp" #include "../HC-1/HC-1.hpp" #include "../presets.hpp" +#include "../square_button.hpp" namespace pachde { @@ -23,13 +24,14 @@ struct Hc3Module : Module }; int loaded_id = 5; //for testing, later -1, meaning (none). - std::string files[16] = {""}; + std::vector files; Hc3Module(); + void clearFiles(); json_t *dataToJson() override; void dataFromJson(json_t *root) override; - + void onReset() override; void process(const ProcessArgs& args) override; }; @@ -37,6 +39,7 @@ struct Hc3ModuleWidget : ModuleWidget { Hc3Module* my_module; Hc3ModuleWidget(Hc3Module* module); + DrawSquareButton drawButton; // IHandleHcEvents // void onPresetChanged(const PresetChangedEvent& e) override; @@ -45,10 +48,9 @@ struct Hc3ModuleWidget : ModuleWidget //void step() override; //void drawExpanderConnector(const DrawArgs& args); void draw(const DrawArgs& args) override; - void drawLayer(const DrawArgs& args, int layer) override; void appendContextMenu(Menu *menu) override; }; -} -#endif \ No newline at end of file +#endif +} \ No newline at end of file diff --git a/src/HC-3/preset_file_widget.hpp b/src/HC-3/preset_file_widget.hpp new file mode 100644 index 0000000..77bbb84 --- /dev/null +++ b/src/HC-3/preset_file_widget.hpp @@ -0,0 +1,149 @@ +#pragma once +#ifndef PRESET_FILE_WIDGET_HPP_INCLUDED +#define PRESET_FILE_WIDGET_HPP_INCLUDED +#include +#include "../tip_widget.hpp" +#include "../open_file.hpp" +#include "../colors.hpp" +#include "HC-3.hpp" + +using namespace ::rack; +namespace pachde { + +struct PresetFileWidget : TipWidget +{ + Hc3Module * my_module; + int id; + DrawSquareButton * drawButton; + bool pressed = false; + + PresetFileWidget() + : my_module(nullptr), id(-1), drawButton(nullptr) + { + box.size.x = 75.f; + box.size.y = 15; + } + void setDrawButton(DrawSquareButton * draw) { + drawButton = draw; + } + void setModule(Hc3Module * module) { my_module = module; } + void setId(int the_id) { + assert(id == -1); + assert(the_id >= 0); + id = the_id; + } + bool haveFile() { return my_module && !my_module->files[id].empty(); } + bool isCurrent() { return my_module && my_module->loaded_id == id; } + std::string getLabel() { return my_module ? system::getStem(my_module->files[id]) : ""; } + + void onDragStart(const DragStartEvent& e) override { + if (e.button == GLFW_MOUSE_BUTTON_LEFT) { + pressed = true; + } + TipWidget::onDragStart(e); + } + + void onDragEnd(const DragEndEvent& e) override + { + TipWidget::onDragEnd(e); + pressed = false; + } + + void onButton(const ButtonEvent& e) override { + TipWidget::onButton(e); + if (!my_module) return; + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0) { + if (isCurrent()) { + my_module->loaded_id = -1; + my_module->getParam(id).setValue(-1); + } else if (haveFile()) { + my_module->loaded_id = id; + my_module->getParam(id).setValue(id); + } + } + } + + void drawLayer(const DrawArgs& args, int layer) override { + TipWidget::drawLayer(args, layer); + if (layer != 1) return; + if (isCurrent() && haveFile()) { + auto vg = args.vg; + auto font = GetPluginFontSemiBold(); + SetTextStyle(vg, font, preset_name_color, 9.f); + nvgTextAlign(vg, NVG_ALIGN_LEFT); + nvgText(vg, 18.f, 10.f, getLabel().c_str(), nullptr); + } + } + + void draw(const DrawArgs& args) override { + TipWidget::draw(args); + auto vg = args.vg; + + drawButton->drawBase(vg); + if (pressed) { + drawButton->drawDownFace(vg); + } else { + drawButton->drawUpFace(vg); + } + if (haveFile()) + { + if (isCurrent()) { + FillRect(vg, 16.5f, 0.f, box.size.x, box.size.y, GetStockColor(StockColor::pachde_blue_dark)); + } else { + auto font = GetPluginFontRegular(); + SetTextStyle(vg, font, RampGray(G_85), 9.f); + nvgTextAlign(vg, NVG_ALIGN_LEFT); + nvgText(vg, 18.f, 10.f, getLabel().c_str(), nullptr); + } + } + } + + void step() override { + TipWidget::step(); + if (!TipWidget::hasText() && haveFile()) { + describe(my_module->files[id]); + } + } + + void appendContextMenu(ui::Menu* menu) override { + if (!my_module) return; + assert(id >= 0); + + std::string friendly_file = my_module->files[id].empty() ? "(none)" : system::getStem(my_module->files[id]); + menu->addChild(createMenuLabel(friendly_file)); + menu->addChild(new MenuSeparator); + + menu->addChild(createMenuItem("Favorite file...", "", [=](){ + std::string path; + std::string folder = asset::user(pluginInstance->slug); + system::createDirectories(folder); + if (openFileDialog( + folder, + "Favorites (.fav):fav;Json (.json):json;Any (*):*))", + "", + path)) { + my_module->files[id] = path; + describe(path); + } + })); + + menu->addChild(createMenuItem(haveFile() ? format_string("Forget %s", friendly_file.c_str()) : "Forget", "", [=](){ + my_module->files[id] = ""; + my_module->loaded_id = -1; + describe(""); + }, !haveFile())); + } +}; + +template +TW* createPFWidget(Vec pos, Hc3Module* module, int id, DrawSquareButton* draw) { + auto w = new TW; + w->box.pos = pos; + w->setModule(module); + w->setId(id); + w->setDrawButton(draw); + return w; +} + +} +#endif \ No newline at end of file diff --git a/src/HcOne.cpp b/src/HcOne.cpp index 6a1a771..340b394 100644 --- a/src/HcOne.cpp +++ b/src/HcOne.cpp @@ -36,17 +36,27 @@ Hc1Module* HcOne::getHc1(std::function pred) auto item = std::find_if(my->hc1s.cbegin(), my->hc1s.cend(), pred); return item == my->hc1s.cend() ? nullptr : *item; } + +void HcOne::scan(std::function pred) +{ + for (auto m: my->hc1s) { + if (!pred(m)) break; + } +} + Hc1Module* HcOne::getHc1(int64_t id) { auto item = std::find_if(my->hc1s.cbegin(), my->hc1s.cend(), [=](Hc1Module* const& m){ return m->getId() == id; }); return item == my->hc1s.cend() ? nullptr : *item; } + void HcOne::registerHc1(Hc1Module* module) { if (my->hc1s.cend() == std::find(my->hc1s.cbegin(), my->hc1s.cend(), module)) { my->hc1s.push_back(module); } } + void HcOne::unregisterHc1(Hc1Module* module) { auto item = std::find(my->hc1s.cbegin(), my->hc1s.cend(), module); diff --git a/src/HcOne.hpp b/src/HcOne.hpp index 43c7510..ccd9aa6 100644 --- a/src/HcOne.hpp +++ b/src/HcOne.hpp @@ -20,6 +20,9 @@ struct HcOne Hc1Module* getHc1(std::function pred); Hc1Module* getHc1(int64_t id); + // pred returns false to stop scan + void scan(std::function pred); + private: HcOne(); }; diff --git a/src/em_picker.hpp b/src/em_picker.hpp new file mode 100644 index 0000000..0304a83 --- /dev/null +++ b/src/em_picker.hpp @@ -0,0 +1,69 @@ +#pragma once +#ifndef EM_PICKER_HPP_INCLUDED +#define EM_PICKER_HPP_INCLUDED +#include +#include "misc.hpp" +#include "plugin.hpp" +#include "tip_widget.hpp" +using namespace ::rack; +namespace pachde { + +struct ISetDevice { + virtual void setMidiDevice(int id) = 0; +}; + +struct EMPicker : TipWidget +{ + midi::Port* port; + widget::FramebufferWidget* fb; + widget::SvgWidget* sw; + ISetDevice* setter; + + EMPicker() { + fb = new widget::FramebufferWidget; + addChild(fb); + sw = new widget::SvgWidget; + fb->addChild(sw); + sw->setSvg(Svg::load(asset::plugin(pluginInstance, "res/MIDI.svg"))); + box.size = sw->box.size; + fb->box.size = sw->box.size; + fb->setDirty(true); + } + + void setExternals(midi::Port* the_port, ISetDevice * callback) { + assert(the_port); + assert(callback); + port = the_port; + setter = callback; + } + + void onButton(const ButtonEvent& e) override + { + TipWidget::onButton(e); + if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == 0) { + createContextMenu(); + e.consume(this); + } + } + + void appendContextMenu(Menu* menu) override + { + if (!port || ! setter) return; + assert(port); + assert(setter); + menu->addChild(createMenuLabel("Eagan Matrix device")); + menu->addChild(new MenuSeparator); + menu->addChild(createMenuItem("Reset (auto)", "", [=](){ setter->setMidiDevice(-1); })); + for (auto id : port->getDeviceIds()) { + std::string name = FilterDeviceName(port->getDeviceName(id)); + if (is_EMDevice(name)) { + menu->addChild(createCheckMenuItem(name, "", + [=](){ return port->getDeviceId() == id; }, + [=](){ setter->setMidiDevice(id); })); + } + } + } +}; + +} +#endif \ No newline at end of file diff --git a/src/hc_events.hpp b/src/hc_events.hpp index f7fa816..1af768d 100644 --- a/src/hc_events.hpp +++ b/src/hc_events.hpp @@ -19,6 +19,14 @@ struct IHandleHcEvents }; virtual void onRoundingChanged(const RoundingChangedEvent& e) = 0; + struct DeviceChangedEvent { + std::string name; + }; + virtual void onDeviceChanged(const DeviceChangedEvent& e) = 0; + + struct DisconnectEvent { }; + virtual void onDisconnect(const DisconnectEvent& e) = 0; + }; } diff --git a/src/indicator_widget.hpp b/src/indicator_widget.hpp new file mode 100644 index 0000000..8b4274c --- /dev/null +++ b/src/indicator_widget.hpp @@ -0,0 +1,43 @@ +#pragma once +#ifndef INDICATOR_WIDGET_INCLUDED +#define INDICATOR_WIDGET_INCLUDED +#include "colors.hpp" +#include "tip_widget.hpp" + +namespace pachde { + +struct IndicatorWidget: TipWidget +{ + std::function get_color; + std::function get_fill; + IndicatorWidget() + : get_color(nullptr), get_fill(nullptr) + { + box.size.x = box.size.y = 5.f; + } + + void draw(const DrawArgs& args) override + { + Dot(args.vg, box.size.x*.5, box.size.y*.5, get_color(), get_fill ? get_fill() : true); + } +}; + +IndicatorWidget * createIndicatorCentered(float x, float y, std::string name, std::function getColor) +{ + IndicatorWidget* w = createWidgetCentered(Vec(x,y)); + w->describe(name); + w->get_color = getColor; + return w; +} + +IndicatorWidget * createIndicatorCentered(float x, float y, std::string name, std::function getColor, std::function getFill) +{ + IndicatorWidget* w = createWidgetCentered(Vec(x,y)); + w->describe(name); + w->get_color = getColor; + w->get_fill = getFill; + return w; +} + +} +#endif \ No newline at end of file diff --git a/src/misc.cpp b/src/misc.cpp index 7d1e987..f76dbce 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -23,6 +23,27 @@ std::string format_string(const char *fmt, ...) return r < 0 ? "??" : s; } +// case-insensitive +bool alpha_order(const std::string& a, const std::string& b) +{ + if (a.empty()) return false; + if (!a.empty() && b.empty()) return true; + auto ita = a.cbegin(); + auto itb = b.cbegin(); + for (; ita != a.cend() && itb != b.cend(); ++ita, ++itb) { + if (*ita == *itb) continue; + auto c1 = std::tolower(*ita); + auto c2 = std::tolower(*itb); + if (c1 == c2) continue; + if (c1 < c2) return true; + return false; + } + if (ita == a.cend() && itb != b.cend()) { + return true; + } + return false; +} + std::size_t common_prefix_length(const std::string& alpha, const std::string& beta) { auto a = alpha.cbegin(), ae = alpha.cend(); auto b = beta.cbegin(), be = beta.cend(); diff --git a/src/misc.hpp b/src/misc.hpp index afc7533..a8db3e9 100644 --- a/src/misc.hpp +++ b/src/misc.hpp @@ -10,6 +10,7 @@ namespace pachde { #endif std::string format_string(const char *fmt, ...); +bool alpha_order(const std::string& a, const std::string& b); size_t common_prefix_length(const std::string& alpha, const std::string& beta); bool is_EMDevice(const std::string& name); std::string FilterDeviceName(std::string text); @@ -35,35 +36,35 @@ enum class InitState { const char * InitStateName(InitState state); -enum Expansion { - None = 0x00, - Left = 0x01, - Right = 0x10, - Both = 0x11 -}; -struct ExpanderPresence { - Expansion exp; - //Expansion operator() () const { return exp; } - ExpanderPresence() : exp(Expansion::None) {} - ExpanderPresence(Expansion e) : exp(e) {} - bool operator == (const ExpanderPresence& rhs) { return exp == rhs.exp; } - bool operator == (const Expansion& rhs) { return exp == rhs; } - static ExpanderPresence fromRackSide(int rackSide) { - return ExpanderPresence(rackSide == 0 ? Expansion::Left : rackSide == 1 ? Expansion:: Right : Expansion::None); - } - void add(Expansion expansion) { exp = static_cast(exp | expansion); } - void remove(Expansion expansion) { exp = static_cast(exp & ~expansion); } - void addRight() { add(Expansion::Right); } - void addLeft() { add(Expansion::Left); } - void removeRight() { remove(Expansion::Right); } - void removeLeft() { remove(Expansion::Left); } - void clear() { exp = Expansion::None; } - bool right() const { return exp & Expansion::Right; } - bool left() const { return exp & Expansion::Left; } - bool both() const { return exp == Expansion::Both; } - bool empty() const { return exp == Expansion::None; } - bool any() { return !empty(); } -}; +// enum Expansion { +// None = 0x00, +// Left = 0x01, +// Right = 0x10, +// Both = 0x11 +// }; +// struct ExpanderPresence { +// Expansion exp; +// //Expansion operator() () const { return exp; } +// ExpanderPresence() : exp(Expansion::None) {} +// ExpanderPresence(Expansion e) : exp(e) {} +// bool operator == (const ExpanderPresence& rhs) { return exp == rhs.exp; } +// bool operator == (const Expansion& rhs) { return exp == rhs; } +// static ExpanderPresence fromRackSide(int rackSide) { +// return ExpanderPresence(rackSide == 0 ? Expansion::Left : rackSide == 1 ? Expansion:: Right : Expansion::None); +// } +// void add(Expansion expansion) { exp = static_cast(exp | expansion); } +// void remove(Expansion expansion) { exp = static_cast(exp & ~expansion); } +// void addRight() { add(Expansion::Right); } +// void addLeft() { add(Expansion::Left); } +// void removeRight() { remove(Expansion::Right); } +// void removeLeft() { remove(Expansion::Left); } +// void clear() { exp = Expansion::None; } +// bool right() const { return exp & Expansion::Right; } +// bool left() const { return exp & Expansion::Left; } +// bool both() const { return exp == Expansion::Both; } +// bool empty() const { return exp == Expansion::None; } +// bool any() { return !empty(); } +// }; struct RateTrigger { diff --git a/src/preset_widget.cpp b/src/preset_widget.cpp index 0c0c519..bd4483a 100644 --- a/src/preset_widget.cpp +++ b/src/preset_widget.cpp @@ -50,7 +50,7 @@ void PresetWidget::draw(const DrawArgs& args) void PresetWidget::appendContextMenu(ui::Menu* menu) { - menu->addChild(createMenuItem(preset->name, "", [](){}, true)); + menu->addChild(createMenuLabel(preset->name)); menu->addChild(new MenuSeparator); if (preset->favorite) { menu->addChild(createMenuItem("Move to first Favorite", "", [this](){ holder->moveFavorite(preset, IPresetHolder::FavoriteMove::First); })); diff --git a/src/preset_widget.hpp b/src/preset_widget.hpp index 9a8cf48..88f1dfe 100644 --- a/src/preset_widget.hpp +++ b/src/preset_widget.hpp @@ -107,13 +107,14 @@ struct PresetWidget : TipWidget } void onButton(const ButtonEvent& e) override { + bool was_pressed = pressed; setPressed(false); TipWidget::onButton(e); if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_RIGHT && (e.mods & RACK_MOD_MASK) == 0) { return; } - if (holder && preset) { + if (was_pressed && holder && preset) { holder->setPreset(preset); } //e.consume(this); diff --git a/src/square_button.hpp b/src/square_button.hpp index 4ce3b20..b9c68f4 100644 --- a/src/square_button.hpp +++ b/src/square_button.hpp @@ -47,8 +47,8 @@ struct ButtonBehavior : TipWidget { } }; -struct SquareButton : ButtonBehavior { - SquareButtonSymbol symbol = SquareButtonSymbol::Funnel; +struct DrawSquareButton +{ NVGcolor base_screen = nvgRGBAf(0.f,0.f,0.f,.75f); NVGcolor med_screen = nvgRGBAf(.4f,.4f,.4f,.65f); NVGcolor hi1 = RampGray(G_75); @@ -56,27 +56,19 @@ struct SquareButton : ButtonBehavior { NVGcolor face1 = RampGray(G_35); NVGcolor face2 = RampGray(G_20); - SquareButton() { - box.size.x = 15.f; - box.size.y = 16.f; - } - - void setSymbol(SquareButtonSymbol sym) { - symbol = sym; - } - void drawUpFace(NVGcontext* vg) { FillRect(vg, 0.5f, 1.f, 14.f, 15.f, base_screen); GradientRect(vg, 0.5f, 0.75f, 14.f, 14.f, hi1, hi2, 0.f, 8.f); GradientRect(vg, .85f, 1.125f, 13.25f, 13.5f, face1, face2, 0.f, 15.f); } + void drawDownFace(NVGcontext* vg) { FillRect(vg, 0.5f, 1.f, 14.f, 14.25f, base_screen); GradientRect(vg, 0.5f, 0.75f, 14.f, 14.f, hi2, hi1, 8.f, 15.f); GradientRect(vg, .85f, 1.125f, 13.25f, 13.5f, face2, face1, 0.f, 15.f); } - void drawFunnel(NVGcontext* vg) { + void drawFunnel(NVGcontext* vg, bool pressed) { nvgBeginPath(vg); nvgMoveTo(vg, 4.f, 4.f); nvgLineTo(vg, 11.f, 4.f ); @@ -135,14 +127,14 @@ struct SquareButton : ButtonBehavior { Line(vg, 7.f, 8.f, 8.f, 8.f, RampGray(G_50), 0.5f); } - void drawSymbol(NVGcontext * vg) { + void drawSymbol(NVGcontext * vg, SquareButtonSymbol symbol, bool pressed) { switch (symbol) { case SquareButtonSymbol::Nil: drawNilSymbol(vg); break; case SquareButtonSymbol::Up: drawUpSymbol(vg); break; case SquareButtonSymbol::Down: drawDownSymbol(vg); break; case SquareButtonSymbol::Left: drawLeftSymbol(vg); break; case SquareButtonSymbol::Right: drawRightSymbol(vg); break; - case SquareButtonSymbol::Funnel: drawFunnel(vg); break; + case SquareButtonSymbol::Funnel: drawFunnel(vg, pressed); break; default: Line(vg, 0, 0, 15, 15, GetStockColor(StockColor::Red)); Line(vg, 15, 0, 0, 15, GetStockColor(StockColor::Red)); @@ -150,21 +142,39 @@ struct SquareButton : ButtonBehavior { } } - void draw(const DrawArgs& args) override { - auto vg = args.vg; + void drawBase(NVGcontext * vg) { FillRect(vg, 0.f, 0.f, 15.f, 15.f, RampGray(G_0)); FillRect(vg, 0.f, 0.f, 15.f, 0.5f, med_screen); //FillRect(vg, 0.f, 14.5f, 15.f, 0.5f, RampGray(G_65)); + } +}; + +struct SquareButton : ButtonBehavior { + SquareButtonSymbol symbol = SquareButtonSymbol::Funnel; + DrawSquareButton image; + + SquareButton() { + box.size.x = 15.f; + box.size.y = 16.f; + } + + void setSymbol(SquareButtonSymbol sym) { + symbol = sym; + } + + void draw(const DrawArgs& args) override { + auto vg = args.vg; + image.drawBase(vg); if (enabled) { if (pressed) { - drawDownFace(vg); + image.drawDownFace(vg); } else { - drawUpFace(vg); + image.drawUpFace(vg); } - drawSymbol(vg); + image.drawSymbol(vg, symbol, pressed); } else { - drawUpFace(vg); - drawNilSymbol(vg); + image.drawUpFace(vg); + image.drawNilSymbol(vg); } } }; diff --git a/src/tip_widget.hpp b/src/tip_widget.hpp index a604931..8c63793 100644 --- a/src/tip_widget.hpp +++ b/src/tip_widget.hpp @@ -44,6 +44,8 @@ struct TipWidget : OpaqueWidget { if (tip_holder) delete tip_holder; tip_holder = nullptr; } + + bool hasText() { return tip_holder && !tip_holder->tip_text.empty(); } void describe(std::string text) {