pluginEditorState.cpp (15388B)
1 #include "pluginEditorState.h" 2 3 #include "pluginEditor.h" 4 #include "pluginProcessor.h" 5 6 #include "baseLib/filesystem.h" 7 8 #include "patchmanager/patchmanager.h" 9 10 #include "juceUiLib/editor.h" 11 #include "juceUiLib/messageBox.h" 12 13 #include "dsp56kEmu/logging.h" 14 15 #include "synthLib/os.h" 16 17 namespace jucePluginEditorLib 18 19 { 20 bridgeLib::PluginDesc getPluginDesc(const pluginLib::Processor& _p) 21 { 22 bridgeLib::PluginDesc pd; 23 _p.getPluginDesc(pd); 24 return pd; 25 } 26 27 PluginEditorState::PluginEditorState(Processor& _processor, pluginLib::Controller& _controller, std::vector<Skin> _includedSkins) 28 : m_processor(_processor), m_parameterBinding(_controller), m_includedSkins(std::move(_includedSkins)) 29 , m_remoteServerList(getPluginDesc(_processor)) 30 { 31 juce::File(getSkinFolder()).createDirectory(); 32 33 // point embedded skins to public data folder if they're not embedded 34 for (auto& skin : m_includedSkins) 35 { 36 if(skin.folder.empty() && !m_processor.findResource(skin.jsonFilename)) 37 skin.folder = baseLib::filesystem::validatePath(getSkinFolder() + skin.displayName); 38 } 39 } 40 41 int PluginEditorState::getWidth() const 42 { 43 return m_editor ? m_editor->getWidth() : 0; 44 } 45 46 int PluginEditorState::getHeight() const 47 { 48 return m_editor ? m_editor->getHeight() : 0; 49 } 50 51 const std::vector<Skin>& PluginEditorState::getIncludedSkins() 52 { 53 return m_includedSkins; 54 } 55 56 juce::Component* PluginEditorState::getUiRoot() const 57 { 58 return m_editor.get(); 59 } 60 61 void PluginEditorState::disableBindings() 62 { 63 m_parameterBinding.disableBindings(); 64 } 65 66 void PluginEditorState::enableBindings() 67 { 68 m_parameterBinding.enableBindings(); 69 } 70 71 void PluginEditorState::loadDefaultSkin() 72 { 73 Skin skin = readSkinFromConfig(); 74 75 if(skin.jsonFilename.empty()) 76 { 77 skin = m_includedSkins[0]; 78 } 79 80 loadSkin(skin); 81 } 82 83 void PluginEditorState::setPerInstanceConfig(const std::vector<uint8_t>& _data) 84 { 85 m_instanceConfig = _data; 86 87 if(m_editor && !m_instanceConfig.empty()) 88 getEditor()->setPerInstanceConfig(m_instanceConfig); 89 } 90 91 void PluginEditorState::getPerInstanceConfig(std::vector<uint8_t>& _data) 92 { 93 if(m_editor) 94 { 95 m_instanceConfig.clear(); 96 getEditor()->getPerInstanceConfig(m_instanceConfig); 97 } 98 99 if(!m_instanceConfig.empty()) 100 _data.insert(_data.end(), m_instanceConfig.begin(), m_instanceConfig.end()); 101 } 102 103 std::string PluginEditorState::getSkinFolder() const 104 { 105 return baseLib::filesystem::validatePath(m_processor.getDataFolder() + "skins/"); 106 } 107 108 bool PluginEditorState::loadSkin(const Skin& _skin, const uint32_t _fallbackIndex/* = 0*/) 109 { 110 if (m_editor) 111 { 112 m_instanceConfig.clear(); 113 getEditor()->getPerInstanceConfig(m_instanceConfig); 114 115 m_parameterBinding.clearBindings(); 116 117 auto* parent = m_editor->getParentComponent(); 118 119 if(parent && parent->getIndexOfChildComponent(m_editor.get()) > -1) 120 parent->removeChildComponent(m_editor.get()); 121 m_editor.reset(); 122 } 123 124 m_rootScale = 1.0f; 125 126 try 127 { 128 auto skin = _skin; 129 130 // if the embedded skin cannot be found, use skin folder as fallback 131 if(_skin.folder.empty() && !m_processor.findResource(_skin.jsonFilename)) 132 { 133 skin.folder = baseLib::filesystem::validatePath(getSkinFolder() + _skin.displayName); 134 } 135 136 auto* editor = createEditor(skin); 137 m_editor.reset(editor); 138 139 getEditor()->onOpenMenu.addListener([this](Editor*, const juce::MouseEvent* _e) 140 { 141 openMenu(_e); 142 }); 143 144 m_rootScale = editor->getScale(); 145 146 m_editor->setTopLeftPosition(0, 0); 147 148 m_currentSkin = _skin; 149 writeSkinToConfig(_skin); 150 151 if(evSkinLoaded) 152 evSkinLoaded(m_editor.get()); 153 154 if(!m_instanceConfig.empty()) 155 getEditor()->setPerInstanceConfig(m_instanceConfig); 156 157 return true; 158 } 159 catch(const std::runtime_error& _err) 160 { 161 LOG("ERROR: Failed to create editor: " << _err.what()); 162 163 genericUI::MessageBox::showOk(juce::MessageBoxIconType::WarningIcon, m_processor.getProperties().name + " - Skin load failed", _err.what()); 164 165 m_parameterBinding.clear(); 166 m_editor.reset(); 167 168 if(_fallbackIndex >= m_includedSkins.size()) 169 return false; 170 171 return loadSkin(m_includedSkins[_fallbackIndex], _fallbackIndex + 1); 172 } 173 } 174 175 void PluginEditorState::setGuiScale(const int _scale) const 176 { 177 if(evSetGuiScale) 178 evSetGuiScale(_scale); 179 } 180 181 Editor* PluginEditorState::getEditor() const 182 { 183 return dynamic_cast<Editor*>(m_editor.get()); 184 } 185 186 void PluginEditorState::openMenu(const juce::MouseEvent* _event) 187 { 188 if(_event && _event->mods.isPopupMenu() && getEditor()) 189 { 190 if(getEditor()->openContextMenuForParameter(_event)) 191 return; 192 } 193 194 const auto& config = m_processor.getConfig(); 195 const auto scale = juce::roundToInt(config.getDoubleValue("scale", 100)); 196 197 juce::PopupMenu menu; 198 199 juce::PopupMenu skinMenu; 200 201 bool loadedSkinIsPartOfList = false; 202 203 std::set<std::pair<std::string, std::string>> knownSkins; // folder, jsonFilename 204 205 auto addSkinEntry = [this, &skinMenu, &loadedSkinIsPartOfList, &knownSkins](const Skin& _skin) 206 { 207 // remove dupes by folder 208 if(!_skin.folder.empty() && !knownSkins.insert({_skin.folder, _skin.jsonFilename}).second) 209 return; 210 211 const auto isCurrent = _skin == getCurrentSkin(); 212 213 if(isCurrent) 214 loadedSkinIsPartOfList = true; 215 216 skinMenu.addItem(_skin.displayName, true, isCurrent,[this, _skin] {loadSkin(_skin);}); 217 }; 218 219 for (const auto & skin : getIncludedSkins()) 220 addSkinEntry(skin); 221 222 bool haveSkinsOnDisk = false; 223 224 // find more skins on disk 225 const auto modulePath = synthLib::getModulePath(); 226 227 // new: user documents folder 228 std::vector<std::string> entries; 229 baseLib::filesystem::getDirectoryEntries(entries, getSkinFolder()); 230 231 // old: next to plugin, kept for backwards compatibility 232 std::vector<std::string> entriesModulePath; 233 baseLib::filesystem::getDirectoryEntries(entriesModulePath, modulePath + "skins_" + m_processor.getProperties().name); 234 entries.insert(entries.end(), entriesModulePath.begin(), entriesModulePath.end()); 235 236 for (const auto& entry : entries) 237 { 238 std::vector<std::string> files; 239 baseLib::filesystem::getDirectoryEntries(files, entry); 240 241 for (const auto& file : files) 242 { 243 if(baseLib::filesystem::hasExtension(file, ".json")) 244 { 245 if(!haveSkinsOnDisk) 246 { 247 haveSkinsOnDisk = true; 248 skinMenu.addSeparator(); 249 } 250 251 std::string skinPath = entry; 252 if(entry.find(modulePath) == 0) 253 skinPath = entry.substr(modulePath.size()); 254 skinPath = baseLib::filesystem::validatePath(skinPath); 255 256 auto jsonName = file; 257 const auto pathEndPos = jsonName.find_last_of("/\\"); 258 if(pathEndPos != std::string::npos) 259 jsonName = file.substr(pathEndPos+1); 260 261 const Skin skin{jsonName.substr(0, jsonName.length() - 5), jsonName, skinPath}; 262 263 addSkinEntry(skin); 264 } 265 } 266 } 267 268 if(!loadedSkinIsPartOfList) 269 addSkinEntry(getCurrentSkin()); 270 271 skinMenu.addSeparator(); 272 273 if(getEditor() && m_currentSkin.folder.empty() || m_currentSkin.folder.find(getSkinFolder()) != 0) 274 { 275 skinMenu.addItem("Export current skin to folder '" + getSkinFolder() + "' on disk", true, false, [this] 276 { 277 exportCurrentSkin(); 278 }); 279 } 280 281 skinMenu.addItem("Open folder '" + getSkinFolder() + "' in File Browser", true, false, [this] 282 { 283 const auto dir = getSkinFolder(); 284 baseLib::filesystem::createDirectory(dir); 285 juce::File(dir).revealToUser(); 286 }); 287 288 juce::PopupMenu scaleMenu; 289 scaleMenu.addItem("50%", true, scale == 50, [this] { setGuiScale(50); }); 290 scaleMenu.addItem("65%", true, scale == 65, [this] { setGuiScale(65); }); 291 scaleMenu.addItem("75%", true, scale == 75, [this] { setGuiScale(75); }); 292 scaleMenu.addItem("85%", true, scale == 85, [this] { setGuiScale(85); }); 293 scaleMenu.addItem("100%", true, scale == 100, [this] { setGuiScale(100); }); 294 scaleMenu.addItem("125%", true, scale == 125, [this] { setGuiScale(125); }); 295 scaleMenu.addItem("150%", true, scale == 150, [this] { setGuiScale(150); }); 296 scaleMenu.addItem("175%", true, scale == 175, [this] { setGuiScale(175); }); 297 scaleMenu.addItem("200%", true, scale == 200, [this] { setGuiScale(200); }); 298 scaleMenu.addItem("250%", true, scale == 250, [this] { setGuiScale(250); }); 299 scaleMenu.addItem("300%", true, scale == 300, [this] { setGuiScale(300); }); 300 301 auto adjustLatency = [this](const int _blocks) 302 { 303 m_processor.setLatencyBlocks(_blocks); 304 305 genericUI::MessageBox::showOk(juce::AlertWindow::WarningIcon, "Warning", 306 "Most hosts cannot handle if a plugin changes its latency while being in use.\n" 307 "It is advised to save, close & reopen the project to prevent synchronization issues."); 308 }; 309 310 const auto latency = m_processor.getPlugin().getLatencyBlocks(); 311 juce::PopupMenu latencyMenu; 312 latencyMenu.addItem("0 (DAW will report proper CPU usage)", true, latency == 0, [this, adjustLatency] { adjustLatency(0); }); 313 latencyMenu.addItem("1 (default)", true, latency == 1, [this, adjustLatency] { adjustLatency(1); }); 314 latencyMenu.addItem("2", true, latency == 2, [this, adjustLatency] { adjustLatency(2); }); 315 latencyMenu.addItem("4", true, latency == 4, [this, adjustLatency] { adjustLatency(4); }); 316 latencyMenu.addItem("8", true, latency == 8, [this, adjustLatency] { adjustLatency(8); }); 317 318 auto servers = m_remoteServerList.getEntries(); 319 320 juce::PopupMenu deviceTypeMenu; 321 deviceTypeMenu.addItem("Local (default)", true, m_processor.getDeviceType() == pluginLib::DeviceType::Local, [this] { m_processor.setDeviceType(pluginLib::DeviceType::Local); }); 322 323 if(servers.empty()) 324 { 325 deviceTypeMenu.addItem("- no servers found -", false, false, [this] {}); 326 } 327 else 328 { 329 for (const auto & server : servers) 330 { 331 if(server.err.code == bridgeLib::ErrorCode::Ok) 332 { 333 std::string name = server.host + ':' + std::to_string(server.serverInfo.portTcp); 334 335 const auto isSelected = m_processor.getDeviceType() == pluginLib::DeviceType::Remote && 336 m_processor.getRemoteDeviceHost() == server.host && 337 m_processor.getRemoteDevicePort() == server.serverInfo.portTcp; 338 339 deviceTypeMenu.addItem(name, true, isSelected, [this, server] 340 { 341 m_processor.setRemoteDevice(server.host, server.serverInfo.portTcp); 342 }); 343 } 344 else 345 { 346 std::string name = server.host + " (error " + std::to_string(static_cast<uint32_t>(server.err.code)) + ", " + server.err.msg + ')'; 347 deviceTypeMenu.addItem(name, false, false, [this] {}); 348 } 349 } 350 } 351 352 menu.addSubMenu("GUI Skin", skinMenu); 353 menu.addSubMenu("GUI Scale", scaleMenu); 354 menu.addSubMenu("Latency (blocks)", latencyMenu); 355 356 if (m_processor.getConfig().getBoolValue("supportDspBridge", false)) 357 menu.addSubMenu("Device Type", deviceTypeMenu); 358 359 menu.addSeparator(); 360 361 auto& regions = m_processor.getController().getParameterDescriptions().getRegions(); 362 363 if(!regions.empty()) 364 { 365 const auto part = m_processor.getController().getCurrentPart(); 366 367 juce::PopupMenu lockRegions; 368 369 auto& locking = m_processor.getController().getParameterLocking(); 370 371 lockRegions.addItem("Unlock All", [&, part] 372 { 373 for (const auto& region : regions) 374 locking.unlockRegion(part, region.first); 375 }); 376 377 lockRegions.addItem("Lock All", [&, part] 378 { 379 for (const auto& region : regions) 380 locking.lockRegion(part, region.first); 381 }); 382 383 lockRegions.addSeparator(); 384 385 uint32_t count = 0; 386 387 std::map<std::string, pluginLib::ParameterRegion> sortedRegions; 388 for (const auto& region : regions) 389 sortedRegions.insert(region); 390 391 for (const auto& region : sortedRegions) 392 { 393 lockRegions.addItem(region.second.getName(), true, m_processor.getController().getParameterLocking().isRegionLocked(part, region.first), [this, id=region.first, part] 394 { 395 auto& locking = m_processor.getController().getParameterLocking(); 396 397 if(locking.isRegionLocked(part, id)) 398 locking.unlockRegion(part, id); 399 else 400 locking.lockRegion(part, id); 401 }); 402 403 if(++count == 16) 404 { 405 lockRegions.addColumnBreak(); 406 count = 0; 407 } 408 } 409 410 menu.addSubMenu("Lock Regions", lockRegions); 411 } 412 413 initContextMenu(menu); 414 415 { 416 menu.addSeparator(); 417 418 juce::PopupMenu panicMenu; 419 420 panicMenu.addItem("Send 'All Notes Off'", [this] 421 { 422 for(uint8_t c=0; c<16; ++c) 423 { 424 synthLib::SMidiEvent ev(synthLib::MidiEventSource::Editor, synthLib::M_CONTROLCHANGE + c, synthLib::MC_ALLNOTESOFF); 425 m_processor.addMidiEvent(ev); 426 } 427 }); 428 429 panicMenu.addItem("Send 'Note Off' for every Note", [this] 430 { 431 for(uint8_t c=0; c<16; ++c) 432 { 433 for(uint8_t n=0; n<128; ++n) 434 { 435 synthLib::SMidiEvent ev(synthLib::MidiEventSource::Editor, synthLib::M_NOTEOFF + c, n, 64, n * 256); 436 m_processor.addMidiEvent(ev); 437 } 438 } 439 }); 440 441 panicMenu.addItem("Reboot Device", [this] 442 { 443 m_processor.rebootDevice(); 444 }); 445 446 menu.addSubMenu("Panic", panicMenu); 447 } 448 449 if(auto* editor = dynamic_cast<Editor*>(getEditor())) 450 { 451 menu.addSeparator(); 452 453 if(auto* pm = editor->getPatchManager()) 454 { 455 #ifdef JUCE_MAC 456 const std::string ctrlName = "Cmd"; 457 #else 458 const std::string ctrlName = "Ctrl"; 459 #endif 460 { 461 juce::PopupMenu::Item item("Copy current Patch to Clipboard"); 462 item.shortcutKeyDescription = ctrlName + "+C"; 463 item.action = [editor] 464 { 465 editor->copyCurrentPatchToClipboard(); 466 }; 467 menu.addItem(item); 468 } 469 470 { 471 auto patches = pm->getPatchesFromClipboard(); 472 if(!patches.empty()) 473 { 474 juce::PopupMenu::Item item("Replace current Patch from Clipboard"); 475 item.shortcutKeyDescription = ctrlName + "+V"; 476 item.action = [editor] 477 { 478 editor->replaceCurrentPatchFromClipboard(); 479 }; 480 menu.addItem(item); 481 } 482 } 483 } 484 } 485 486 { 487 const auto allowAdvanced = config.getBoolValue("allow_advanced_options", false); 488 489 juce::PopupMenu advancedMenu; 490 advancedMenu.addItem("Enable Advanced Options", true, allowAdvanced, [this, allowAdvanced] 491 { 492 if(!allowAdvanced) 493 { 494 genericUI::MessageBox::showOkCancel( 495 juce::MessageBoxIconType::WarningIcon, 496 "Warning", 497 "Changing these settings may cause instability of the plugin.\n\nPlease confirm to continue.", 498 [this](const genericUI::MessageBox::Result _result) 499 { 500 if (_result == genericUI::MessageBox::Result::Ok) 501 m_processor.getConfig().setValue("allow_advanced_options", true); 502 }); 503 } 504 else 505 { 506 m_processor.getConfig().setValue("allow_advanced_options", juce::var(false)); 507 } 508 }); 509 510 advancedMenu.addSeparator(); 511 512 if(initAdvancedContextMenu(advancedMenu, allowAdvanced)) 513 { 514 menu.addSeparator(); 515 menu.addSubMenu("Advanced...", advancedMenu); 516 } 517 } 518 519 menu.showMenuAsync(juce::PopupMenu::Options()); 520 } 521 522 void PluginEditorState::exportCurrentSkin() const 523 { 524 auto* editor = getEditor(); 525 526 if(!editor) 527 return; 528 529 const auto res = editor->exportToFolder(getSkinFolder()); 530 531 if(!res.empty()) 532 { 533 genericUI::MessageBox::showOk(juce::MessageBoxIconType::WarningIcon, "Export failed", "Failed to export skin:\n\n" + res, editor); 534 } 535 else 536 { 537 genericUI::MessageBox::showOk(juce::MessageBoxIconType::InfoIcon, "Export finished", "Skin successfully exported", editor); 538 } 539 } 540 541 Skin PluginEditorState::readSkinFromConfig() const 542 { 543 const auto& config = m_processor.getConfig(); 544 545 Skin skin; 546 skin.displayName = config.getValue("skinDisplayName", "").toStdString(); 547 skin.jsonFilename = config.getValue("skinFile", "").toStdString(); 548 skin.folder = config.getValue("skinFolder", "").toStdString(); 549 return skin; 550 } 551 552 void PluginEditorState::writeSkinToConfig(const Skin& _skin) const 553 { 554 auto& config = m_processor.getConfig(); 555 556 config.setValue("skinDisplayName", _skin.displayName.c_str()); 557 config.setValue("skinFile", _skin.jsonFilename.c_str()); 558 config.setValue("skinFolder", _skin.folder.c_str()); 559 } 560 561 }