gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

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 }