pluginEditor.cpp (19730B)
1 #include "pluginEditor.h" 2 3 #include "pluginProcessor.h" 4 #include "skin.h" 5 6 #include "baseLib/filesystem.h" 7 8 #include "jucePluginLib/clipboard.h" 9 #include "jucePluginLib/filetype.h" 10 #include "jucePluginLib/parameterbinding.h" 11 #include "jucePluginLib/tools.h" 12 13 #include "juceUiLib/messageBox.h" 14 15 #include "synthLib/os.h" 16 #include "synthLib/sysexToMidi.h" 17 18 #include "patchmanager/patchmanager.h" 19 #include "patchmanager/savepatchdesc.h" 20 21 namespace jucePluginEditorLib 22 { 23 Editor::Editor(Processor& _processor, pluginLib::ParameterBinding& _binding, Skin _skin) 24 : genericUI::Editor(static_cast<EditorInterface&>(*this)) 25 , m_processor(_processor) 26 , m_binding(_binding) 27 , m_skin(std::move(_skin)) 28 , m_overlays(*this, _binding) 29 { 30 showDisclaimer(); 31 } 32 33 Editor::~Editor() 34 { 35 for (const auto& file : m_dragAndDropFiles) 36 file.deleteFile(); 37 } 38 39 void Editor::create() 40 { 41 genericUI::Editor::create(m_skin.jsonFilename); 42 } 43 44 const char* Editor::findResourceByFilename(const std::string& _filename, uint32_t& _size) const 45 { 46 const auto res = m_processor.findResource(_filename); 47 if(!res) 48 return nullptr; 49 _size = res->second; 50 return res->first; 51 } 52 53 void Editor::loadPreset(const std::function<void(const juce::File&)>& _callback) 54 { 55 const auto path = m_processor.getConfig().getValue("load_path", ""); 56 57 m_fileChooser = std::make_unique<juce::FileChooser>( 58 "Choose syx/midi banks to import", 59 path.isEmpty() ? juce::File::getSpecialLocation(juce::File::currentApplicationFile).getParentDirectory() : path, 60 "*.syx,*.mid,*.midi,*.vstpreset,*.fxb,*.cpr", true); 61 62 constexpr auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::FileChooserFlags::canSelectFiles; 63 64 const std::function onFileChosen = [this, _callback](const juce::FileChooser& _chooser) 65 { 66 if (_chooser.getResults().isEmpty()) 67 return; 68 69 const auto result = _chooser.getResult(); 70 71 m_processor.getConfig().setValue("load_path", result.getParentDirectory().getFullPathName()); 72 73 _callback(result); 74 }; 75 m_fileChooser->launchAsync(flags, onFileChosen); 76 } 77 78 void Editor::savePreset(const pluginLib::FileType& _fileType, const std::function<void(const juce::File&)>& _callback) 79 { 80 #if !SYNTHLIB_DEMO_MODE 81 const auto path = m_processor.getConfig().getValue("save_path", ""); 82 83 m_fileChooser = std::make_unique<juce::FileChooser>( 84 "Save preset(s) as " + _fileType.type(), 85 path.isEmpty() ? juce::File::getSpecialLocation(juce::File::currentApplicationFile).getParentDirectory() : path, 86 "*." + _fileType.type(), true); 87 88 constexpr auto flags = juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::FileChooserFlags::canSelectFiles; 89 90 auto onFileChosen = [this, _callback](const juce::FileChooser& _chooser) 91 { 92 if (_chooser.getResults().isEmpty()) 93 return; 94 95 const auto result = _chooser.getResult(); 96 m_processor.getConfig().setValue("save_path", result.getParentDirectory().getFullPathName()); 97 98 if (!result.existsAsFile()) 99 { 100 _callback(result); 101 } 102 else 103 { 104 genericUI::MessageBox::showYesNo(juce::MessageBoxIconType::WarningIcon, "File exists", "Do you want to overwrite the existing file?", 105 [this, _callback, result](const genericUI::MessageBox::Result _result) 106 { 107 if (_result == genericUI::MessageBox::Result::Yes) 108 _callback(result); 109 }); 110 } 111 }; 112 m_fileChooser->launchAsync(flags, onFileChosen); 113 #else 114 showDemoRestrictionMessageBox(); 115 #endif 116 } 117 118 #if !SYNTHLIB_DEMO_MODE 119 bool Editor::savePresets(const pluginLib::FileType& _type, const std::string& _pathName, const std::vector<std::vector<uint8_t>>& _presets) 120 { 121 if (_presets.empty()) 122 return false; 123 124 if (_type == pluginLib::FileType::Mid) 125 return synthLib::SysexToMidi::write(_pathName.c_str(), _presets); 126 127 FILE* hFile = fopen(_pathName.c_str(), "wb"); 128 129 if (!hFile) 130 return false; 131 132 for (const auto& message : _presets) 133 { 134 const auto written = fwrite(&message.front(), 1, message.size(), hFile); 135 136 if (written != message.size()) 137 { 138 fclose(hFile); 139 return false; 140 } 141 } 142 fclose(hFile); 143 return true; 144 } 145 #endif 146 147 std::string Editor::createValidFilename(pluginLib::FileType& _type, const juce::File& _file) 148 { 149 const auto ext = _file.getFileExtension(); 150 auto file = _file.getFullPathName().toStdString(); 151 152 if (ext.endsWithIgnoreCase(_type.type())) 153 return file; 154 155 if (ext.endsWithIgnoreCase("mid")) 156 _type = pluginLib::FileType::Mid; 157 else if (ext.endsWithIgnoreCase("syx")) 158 _type = pluginLib::FileType::Syx; 159 else 160 file += "." + _type.type(); 161 return file; 162 } 163 164 void Editor::showDemoRestrictionMessageBox() const 165 { 166 const auto &[title, msg] = getDemoRestrictionText(); 167 genericUI::MessageBox::showOk(juce::AlertWindow::WarningIcon, title, msg); 168 } 169 170 void Editor::setPatchManager(patchManager::PatchManager* _patchManager) 171 { 172 m_patchManager.reset(_patchManager); 173 174 if(_patchManager && !m_patchManagerConfig.empty()) 175 m_patchManager->setPerInstanceConfig(m_patchManagerConfig); 176 } 177 178 void Editor::setPerInstanceConfig(const std::vector<uint8_t>& _data) 179 { 180 { 181 // test if its an old version that didn't use chunks yet 182 pluginLib::PluginStream oldStream(_data); 183 const auto version = oldStream.read<uint32_t>(); 184 185 if(version == 1) 186 { 187 m_patchManagerConfig = _data; 188 if(m_patchManager) 189 m_patchManager->setPerInstanceConfig(_data); 190 return; 191 } 192 } 193 194 baseLib::BinaryStream s(_data); 195 baseLib::ChunkReader cr(s); 196 loadChunkData(cr); 197 cr.read(); 198 } 199 200 void Editor::loadChunkData(baseLib::ChunkReader& _cr) 201 { 202 _cr.add("pmDt", 2, [this](baseLib::BinaryStream& _s, uint32_t/* _version*/) 203 { 204 m_patchManagerConfig.clear(); 205 _s.read(m_patchManagerConfig); 206 if(m_patchManager) 207 m_patchManager->setPerInstanceConfig(m_patchManagerConfig); 208 }); 209 } 210 211 void Editor::getPerInstanceConfig(std::vector<uint8_t>& _data) 212 { 213 baseLib::BinaryStream s; 214 saveChunkData(s); 215 s.toVector(_data); 216 } 217 218 void Editor::saveChunkData(baseLib::BinaryStream& _s) 219 { 220 if(m_patchManager) 221 { 222 m_patchManagerConfig.clear(); 223 m_patchManager->getPerInstanceConfig(m_patchManagerConfig); 224 } 225 baseLib::ChunkWriter cw(_s, "pmDt", 2); 226 _s.write(m_patchManagerConfig); 227 } 228 229 void Editor::setCurrentPart(const uint8_t _part) 230 { 231 genericUI::Editor::setCurrentPart(_part); 232 233 if(m_patchManager) 234 m_patchManager->setCurrentPart(_part); 235 } 236 237 void Editor::showDisclaimer() const 238 { 239 if(pluginLib::Tools::isHeadless()) 240 return; 241 242 if(!m_processor.getConfig().getBoolValue("disclaimerSeen", false)) 243 { 244 const juce::MessageBoxOptions options = juce::MessageBoxOptions::makeOptionsOk(juce::MessageBoxIconType::WarningIcon, m_processor.getProperties().name, 245 "It is the sole responsibility of the user to operate this emulator within the bounds of all applicable laws.\n\n" 246 247 "Usage of emulators in conjunction with ROM images you are not legally entitled to own is forbidden by copyright law.\n\n" 248 249 "If you are not legally entitled to use this emulator please discontinue usage immediately.\n\n", 250 251 "I Agree" 252 ); 253 254 juce::NativeMessageBox::showAsync(options, [this](int) 255 { 256 m_processor.getConfig().setValue("disclaimerSeen", true); 257 onDisclaimerFinished(); 258 }); 259 } 260 else 261 { 262 onDisclaimerFinished(); 263 } 264 } 265 266 bool Editor::shouldDropFilesWhenDraggedExternally(const juce::DragAndDropTarget::SourceDetails& sourceDetails, juce::StringArray& files, bool& canMoveFiles) 267 { 268 const auto* ddObject = DragAndDropObject::fromDragSource(sourceDetails); 269 270 if(!ddObject || !ddObject->canDropExternally()) 271 return false; 272 273 // try to create human-readable filename first 274 const auto patchFileName = ddObject->getExportFileName(m_processor); 275 const auto pathName = juce::File::getSpecialLocation(juce::File::tempDirectory).getFullPathName().toStdString() + "/" + patchFileName; 276 277 auto file = juce::File(pathName); 278 279 if(file.hasWriteAccess()) 280 { 281 m_dragAndDropFiles.emplace_back(file); 282 } 283 else 284 { 285 // failed, create temp file 286 const auto& tempFile = m_dragAndDropTempFiles.emplace_back(std::make_shared<juce::TemporaryFile>(patchFileName)); 287 file = tempFile->getFile(); 288 } 289 290 if(!ddObject->writeToFile(file)) 291 return false; 292 293 files.add(file.getFullPathName()); 294 295 canMoveFiles = true; 296 return true; 297 } 298 299 void Editor::copyCurrentPatchToClipboard() const 300 { 301 // copy patch of current part to Clipboard 302 if(!m_patchManager) 303 return; 304 305 const auto p = m_patchManager->requestPatchForPart(m_patchManager->getCurrentPart()); 306 307 if(!p) 308 return; 309 310 const auto patchAsString = m_patchManager->toString(p, pluginLib::FileType::Empty, pluginLib::ExportType::Clipboard); 311 312 if(!patchAsString.empty()) 313 juce::SystemClipboard::copyTextToClipboard(patchAsString); 314 } 315 316 bool Editor::replaceCurrentPatchFromClipboard() const 317 { 318 if(!m_patchManager) 319 return false; 320 return m_patchManager->activatePatchFromClipboard(); 321 } 322 323 void Editor::openMenu(juce::MouseEvent* _event) 324 { 325 onOpenMenu(this, _event); 326 } 327 328 bool Editor::openContextMenuForParameter(const juce::MouseEvent* _event) 329 { 330 if(!_event || !_event->originalComponent) 331 return false; 332 333 const auto* param = m_binding.getBoundParameter(_event->originalComponent); 334 if(!param) 335 return false; 336 337 auto& controller = m_processor.getController(); 338 339 const auto& regions = controller.getParameterDescriptions().getRegions(); 340 const auto paramRegionIds = controller.getRegionIdsForParameter(param); 341 342 if(paramRegionIds.empty()) 343 return false; 344 345 const auto part = param->getPart(); 346 347 juce::PopupMenu menu; 348 349 // Lock / Unlock 350 351 for (const auto& regionId : paramRegionIds) 352 { 353 const auto& regionName = regions.find(regionId)->second.getName(); 354 355 const auto isLocked = controller.getParameterLocking().isRegionLocked(part, regionId); 356 357 menu.addItem(std::string(isLocked ? "Unlock" : "Lock") + std::string(" region '") + regionName + "'", [this, regionId, isLocked, part] 358 { 359 auto& locking = m_processor.getController().getParameterLocking(); 360 if(isLocked) 361 locking.unlockRegion(part, regionId); 362 else 363 locking.lockRegion(part, regionId); 364 }); 365 } 366 367 // Copy to clipboard 368 369 menu.addSeparator(); 370 371 for (const auto& regionId : paramRegionIds) 372 { 373 const auto& regionName = regions.find(regionId)->second.getName(); 374 375 menu.addItem(std::string("Copy region '") + regionName + "'", [this, regionId] 376 { 377 copyRegionToClipboard(regionId); 378 }); 379 } 380 381 // Paste from clipboard 382 383 const auto data = pluginLib::Clipboard::getDataFromString(m_processor, juce::SystemClipboard::getTextFromClipboard().toStdString()); 384 385 if(!data.parameterValuesByRegion.empty()) 386 { 387 bool haveSeparator = false; 388 389 for (const auto& paramRegionId : paramRegionIds) 390 { 391 const auto it = data.parameterValuesByRegion.find(paramRegionId); 392 393 if(it == data.parameterValuesByRegion.end()) 394 continue; 395 396 // if region is not fully covered, skip it 397 const auto& region = regions.find(it->first)->second; 398 if(it->second.size() < region.getParams().size()) 399 continue; 400 401 const auto& parameterValues = it->second; 402 403 if(!haveSeparator) 404 { 405 menu.addSeparator(); 406 haveSeparator = true; 407 } 408 409 const auto& regionName = regions.find(paramRegionId)->second.getName(); 410 411 menu.addItem("Paste region '" + regionName + "'", [this, parameterValues] 412 { 413 setParameters(parameterValues); 414 }); 415 } 416 417 menu.addSeparator(); 418 419 const auto& desc = param->getDescription(); 420 const auto& paramName = desc.name; 421 422 const auto itParam = data.parameterValues.find(paramName); 423 424 if(itParam != data.parameterValues.end()) 425 { 426 const auto& paramValue = itParam->second; 427 428 const auto& valueText = desc.valueList.valueToText(paramValue); 429 430 menu.addItem("Paste value '" + valueText + "' for parameter '" + desc.displayName + "'", [this, paramName, paramValue] 431 { 432 pluginLib::Clipboard::Data::ParameterValues params; 433 params.insert({paramName, paramValue}); 434 setParameters(params); 435 }); 436 } 437 } 438 439 // Parameter links 440 441 juce::PopupMenu linkMenu; 442 443 menu.addSeparator(); 444 445 for (const auto& regionId : paramRegionIds) 446 { 447 juce::PopupMenu regionMenu; 448 449 const auto currentPart = controller.getCurrentPart(); 450 451 for(uint8_t p=0; p<controller.getPartCount(); ++p) 452 { 453 if(p == currentPart) 454 continue; 455 456 const auto isLinked = controller.getParameterLinks().isRegionLinked(regionId, currentPart, p); 457 458 regionMenu.addItem(std::string("Link Part ") + std::to_string(p+1), true, isLinked, [this, regionId, isLinked, currentPart, p] 459 { 460 auto& links = m_processor.getController().getParameterLinks(); 461 462 if(isLinked) 463 links.unlinkRegion(regionId, currentPart, p); 464 else 465 links.linkRegion(regionId, currentPart, p, true); 466 }); 467 } 468 469 const auto& regionName = regions.find(regionId)->second.getName(); 470 linkMenu.addSubMenu("Region '" + regionName + "'", regionMenu); 471 } 472 473 menu.addSubMenu("Parameter Links", linkMenu); 474 475 auto& midiPackets = m_processor.getController().getParameterDescriptions().getMidiPackets(); 476 for (const auto& mp : midiPackets) 477 { 478 auto defIndices = mp.second.getDefinitionIndicesForParameterName(param->getDescription().name); 479 if (defIndices.empty()) 480 continue; 481 482 const auto expectedValue = param->getUnnormalizedValue(); 483 484 menu.addSeparator(); 485 486 auto findSimilar = [this, defIndices, packet = &mp.second, expectedValue](const int _offsetMin, const int _offsetMax) 487 { 488 pluginLib::patchDB::SearchRequest sr; 489 490 sr.customCompareFunc = [packet, expectedValue, defIndices, _offsetMin, _offsetMax](const pluginLib::patchDB::Patch& _patch) -> bool 491 { 492 if (_patch.sysex.empty()) 493 return false; 494 const auto v = packet->getParameterValue(_patch.sysex, defIndices); 495 if (v >= expectedValue + _offsetMin && v <= expectedValue + _offsetMax) 496 return true; 497 return false; 498 }; 499 500 const auto sh = getPatchManager()->search(std::move(sr)); 501 502 if (sh != pluginLib::patchDB::g_invalidSearchHandle) 503 { 504 getPatchManager()->setCustomSearch(sh); 505 getPatchManager()->bringToFront(); 506 } 507 }; 508 509 juce::PopupMenu subMenu; 510 subMenu.addItem("Exact Match (Value " + param->getCurrentValueAsText() + ")", [this, findSimilar]{ findSimilar(0, 0); }); 511 subMenu.addItem("-/+ 4", [this, findSimilar]{ findSimilar(-4, 4); }); 512 subMenu.addItem("-/+ 12", [this, findSimilar]{ findSimilar(-12, 12); }); 513 subMenu.addItem("-/+ 24", [this, findSimilar]{ findSimilar(-24, 24); }); 514 515 menu.addSubMenu("Find similar Patches for parameter " + param->getDescription().displayName, subMenu); 516 517 break; 518 } 519 menu.showMenuAsync({}); 520 521 return true; 522 } 523 524 bool Editor::copyRegionToClipboard(const std::string& _regionId) const 525 { 526 const auto& regions = m_processor.getController().getParameterDescriptions().getRegions(); 527 const auto it = regions.find(_regionId); 528 if(it == regions.end()) 529 return false; 530 531 const auto& region = it->second; 532 533 const auto& params = region.getParams(); 534 535 std::vector<std::string> paramsList; 536 paramsList.reserve(params.size()); 537 538 for (const auto& p : params) 539 paramsList.push_back(p.first); 540 541 return copyParametersToClipboard(paramsList, _regionId); 542 } 543 544 bool Editor::copyParametersToClipboard(const std::vector<std::string>& _params, const std::string& _regionId) const 545 { 546 const auto result = pluginLib::Clipboard::parametersToString(m_processor, _params, _regionId); 547 548 if(result.empty()) 549 return false; 550 551 juce::SystemClipboard::copyTextToClipboard(result); 552 553 return true; 554 } 555 556 bool Editor::setParameters(const std::map<std::string, pluginLib::ParamValue>& _paramValues) const 557 { 558 if(_paramValues.empty()) 559 return false; 560 561 return getProcessor().getController().setParameters(_paramValues, m_processor.getController().getCurrentPart(), pluginLib::Parameter::Origin::Ui); 562 } 563 564 void Editor::parentHierarchyChanged() 565 { 566 genericUI::Editor::parentHierarchyChanged(); 567 568 if(isShowing()) 569 m_overlays.refreshAll(); 570 } 571 572 juce::PopupMenu Editor::createExportFileTypeMenu(const std::function<void(pluginLib::FileType)>& _func) const 573 { 574 juce::PopupMenu menu; 575 createExportFileTypeMenu(menu, _func); 576 return menu; 577 } 578 579 void Editor::createExportFileTypeMenu(juce::PopupMenu& _menu, const std::function<void(pluginLib::FileType)>& _func) const 580 { 581 _menu.addItem(".syx", [this, _func]{_func(pluginLib::FileType::Syx);}); 582 _menu.addItem(".mid", [this, _func]{_func(pluginLib::FileType::Mid);}); 583 } 584 585 bool Editor::keyPressed(const juce::KeyPress& _key) 586 { 587 if(_key.getModifiers().isCommandDown()) 588 { 589 switch(_key.getKeyCode()) 590 { 591 case 'c': 592 case 'C': 593 copyCurrentPatchToClipboard(); 594 return true; 595 case 'v': 596 case 'V': 597 if(replaceCurrentPatchFromClipboard()) 598 return true; 599 break; 600 default: 601 return genericUI::Editor::keyPressed(_key); 602 } 603 } 604 return genericUI::Editor::keyPressed(_key); 605 } 606 607 void Editor::onDisclaimerFinished() const 608 { 609 if(!synthLib::isRunningUnderRosetta()) 610 return; 611 612 const auto& name = m_processor.getProperties().name; 613 614 genericUI::MessageBox::showOk(juce::MessageBoxIconType::WarningIcon, 615 name + " - Rosetta detected", 616 name + " appears to be running in Rosetta mode.\n" 617 "\n" 618 "The DSP emulation core will perform much worse when being executed under Rosetta. We strongly recommend to run your DAW as a native Apple Silicon application"); 619 } 620 621 const char* Editor::getResourceByFilename(const std::string& _name, uint32_t& _dataSize) 622 { 623 if(!m_skin.folder.empty()) 624 { 625 auto readFromCache = [this, &_name, &_dataSize]() 626 { 627 const auto it = m_fileCache.find(_name); 628 if(it == m_fileCache.end()) 629 { 630 _dataSize = 0; 631 return static_cast<char*>(nullptr); 632 } 633 _dataSize = static_cast<uint32_t>(it->second.size()); 634 return &it->second.front(); 635 }; 636 637 const auto* res = readFromCache(); 638 639 if(res) 640 return res; 641 642 const auto modulePath = synthLib::getModulePath(); 643 const auto publicDataPath = m_processor.getDataFolder(); 644 const auto folder = baseLib::filesystem::validatePath(m_skin.folder.find(modulePath) == 0 || m_skin.folder.find(publicDataPath) == 0 ? m_skin.folder : modulePath + m_skin.folder); 645 646 // try to load from disk first 647 FILE* hFile = fopen((folder + _name).c_str(), "rb"); 648 if(hFile) 649 { 650 fseek(hFile, 0, SEEK_END); 651 _dataSize = ftell(hFile); 652 fseek(hFile, 0, SEEK_SET); 653 654 std::vector<char> data; 655 data.resize(_dataSize); 656 const auto readCount = fread(&data.front(), 1, _dataSize, hFile); 657 fclose(hFile); 658 659 if(readCount == _dataSize) 660 m_fileCache.insert(std::make_pair(_name, std::move(data))); 661 662 res = readFromCache(); 663 664 if(res) 665 return res; 666 } 667 } 668 669 uint32_t size = 0; 670 const auto res = findResourceByFilename(_name, size); 671 if(!res) 672 throw std::runtime_error("Failed to find file named " + _name); 673 _dataSize = size; 674 return res; 675 } 676 677 int Editor::getParameterIndexByName(const std::string& _name) 678 { 679 return static_cast<int>(m_processor.getController().getParameterIndexByName(_name)); 680 } 681 682 bool Editor::bindParameter(juce::Button& _target, int _parameterIndex) 683 { 684 m_binding.bind(_target, _parameterIndex); 685 return true; 686 } 687 688 bool Editor::bindParameter(juce::ComboBox& _target, int _parameterIndex) 689 { 690 m_binding.bind(_target, _parameterIndex); 691 return true; 692 } 693 694 bool Editor::bindParameter(juce::Slider& _target, int _parameterIndex) 695 { 696 m_binding.bind(_target, _parameterIndex); 697 return true; 698 } 699 700 bool Editor::bindParameter(juce::Label& _target, int _parameterIndex) 701 { 702 m_binding.bind(_target, _parameterIndex); 703 return true; 704 } 705 706 juce::Value* Editor::getParameterValue(int _parameterIndex, uint8_t _part) 707 { 708 return m_processor.getController().getParamValueObject(_parameterIndex, _part); 709 } 710 }