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

patchmanager.cpp (30011B)


      1 #include "patchmanager.h"
      2 
      3 #include "datasourcetree.h"
      4 #include "datasourcetreeitem.h"
      5 #include "grid.h"
      6 #include "info.h"
      7 #include "list.h"
      8 #include "listmodel.h"
      9 #include "searchlist.h"
     10 #include "searchtree.h"
     11 #include "status.h"
     12 #include "tagstree.h"
     13 #include "tree.h"
     14 
     15 #include "../pluginEditor.h"
     16 #include "../pluginProcessor.h"
     17 
     18 #include "baseLib/filesystem.h"
     19 
     20 #include "jucePluginLib/clipboard.h"
     21 #include "jucePluginLib/filetype.h"
     22 #include "jucePluginLib/types.h"
     23 
     24 #include "dsp56kEmu/logging.h"
     25 
     26 #if JUCE_MAJOR_VERSION < 8	// they forgot this include but fixed it in version 8+
     27 #include "juce_gui_extra/misc/juce_ColourSelector.h"
     28 #endif
     29 
     30 namespace jucePluginEditorLib::patchManager
     31 {
     32 	constexpr int g_scale = 2;
     33 	constexpr auto g_searchBarHeight = 32;
     34 	constexpr int g_padding = 4;
     35 
     36 	PatchManager::PatchManager(Editor& _editor, Component* _root, const std::initializer_list<GroupType>& _groupTypes)
     37 	: DB(juce::File(_editor.getProcessor().getPatchManagerDataFolder(false)))
     38 	, m_editor(_editor)
     39 	, m_state(*this)
     40 	{
     41 		setTagTypeName(pluginLib::patchDB::TagType::Category, "Category");
     42 		setTagTypeName(pluginLib::patchDB::TagType::Tag, "Tag");
     43 		setTagTypeName(pluginLib::patchDB::TagType::Favourites, "Favourite");
     44 
     45 		const auto rootW = _root->getWidth() / g_scale;
     46 		const auto rootH = _root->getHeight() / g_scale;
     47 		const auto scale = juce::AffineTransform::scale(g_scale);
     48 
     49 		setSize(rootW, rootH);
     50 		setTransform(scale);
     51 
     52 		_root->addAndMakeVisible(this);
     53 
     54 		auto weight = [&](int _weight)
     55 		{
     56 			return rootW * _weight / 100;
     57 		};
     58 
     59 		// 1st column
     60 		auto w = weight(33);
     61 		m_treeDS = new DatasourceTree(*this, _groupTypes);
     62 		m_treeDS->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding);
     63 
     64 		m_searchTreeDS = new SearchTree(*m_treeDS);
     65 		m_searchTreeDS->setSize(m_treeDS->getWidth(), g_searchBarHeight);
     66 		m_searchTreeDS->setTopLeftPosition(m_treeDS->getX(), m_treeDS->getHeight() + g_padding);
     67 
     68 		addAndMakeVisible(m_treeDS);
     69 		addAndMakeVisible(m_searchTreeDS);
     70 
     71 		// 2nd column
     72 		w = weight(20);
     73 		m_treeTags = new TagsTree(*this);
     74 		m_treeTags->setTopLeftPosition(m_treeDS->getRight() + g_padding, 0);
     75 		m_treeTags->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding);
     76 
     77 		m_searchTreeTags = new SearchTree(*m_treeTags);
     78 		m_searchTreeTags->setTopLeftPosition(m_treeTags->getX(), m_treeTags->getHeight() + g_padding);
     79 		m_searchTreeTags->setSize(m_treeTags->getWidth(), g_searchBarHeight);
     80 
     81 		addAndMakeVisible(m_treeTags);
     82 		addAndMakeVisible(m_searchTreeTags);
     83 
     84 		// 3rd column
     85 		w = weight(15);
     86 		m_list = new List(*this);
     87 		m_list->setTopLeftPosition(m_treeTags->getRight() + g_padding, 0);
     88 		m_list->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding);
     89 
     90 		m_grid = new Grid(*this);
     91 		m_grid->setTopLeftPosition(m_list->getPosition());
     92 		m_grid->setSize(m_list->getWidth(), m_list->getHeight());
     93 
     94 		m_grid->setLookAndFeel(&m_list->getLookAndFeel());
     95 		m_grid->setColour(juce::ListBox::backgroundColourId, m_list->findColour(juce::ListBox::backgroundColourId));
     96 		m_grid->setColour(juce::ListBox::textColourId, m_list->findColour(juce::ListBox::textColourId));
     97 		m_grid->setColour(juce::ListBox::outlineColourId, m_list->findColour(juce::ListBox::outlineColourId));
     98 
     99 		m_searchList = new SearchList(*m_list);
    100 		m_searchList->setTopLeftPosition(m_list->getX(), m_list->getHeight() + g_padding);
    101 		m_searchList->setSize(m_list->getWidth(), g_searchBarHeight);
    102 
    103 		addAndMakeVisible(m_list);
    104 		addChildComponent(m_grid);
    105 		addAndMakeVisible(m_searchList);
    106 
    107 		// 4th column
    108 		m_info = new Info(*this);
    109 		m_info->setTopLeftPosition(m_list->getRight() + g_padding, 0);
    110 		m_info->setSize(getWidth() - m_info->getX(), rootH - g_searchBarHeight - g_padding);
    111 
    112 		m_status = new Status();
    113 		m_status->setTopLeftPosition(m_info->getX(), m_info->getHeight() + g_padding);
    114 		m_status->setSize(m_info->getWidth(), g_searchBarHeight);
    115 
    116 		addAndMakeVisible(m_info);
    117 		addAndMakeVisible(m_status);
    118 
    119 		if(const auto t = getTemplate("pm_search"))
    120 		{
    121 			t->apply(getEditor(), *m_searchList);
    122 			t->apply(getEditor(), *m_searchTreeDS);
    123 			t->apply(getEditor(), *m_searchTreeTags);
    124 		}
    125 
    126 		m_searchList->setTextToShowWhenEmpty("Search...", m_searchList->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
    127 		m_searchTreeDS->setTextToShowWhenEmpty("Search...", m_searchTreeDS->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
    128 		m_searchTreeTags->setTextToShowWhenEmpty("Search...", m_searchTreeTags->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
    129 
    130 		if(const auto t = getTemplate("pm_status_label"))
    131 		{
    132 			t->apply(getEditor(), *m_status);
    133 		}
    134 
    135 		juce::StretchableLayoutManager lm;
    136 
    137 		m_stretchableManager.setItemLayout(0, 100, rootW * 0.5, m_treeDS->getWidth());		m_stretchableManager.setItemLayout(1, 5, 5, 5);
    138 		m_stretchableManager.setItemLayout(2, 100, rootW * 0.5, m_treeTags->getWidth());	m_stretchableManager.setItemLayout(3, 5, 5, 5);
    139 		m_stretchableManager.setItemLayout(4, 100, rootW * 0.5, m_list->getWidth());		m_stretchableManager.setItemLayout(5, 5, 5, 5);
    140 		m_stretchableManager.setItemLayout(6, 100, rootW * 0.5, m_info->getWidth());
    141 
    142 		m_resizerBarA.setSize(5, rootH);
    143 		m_resizerBarB.setSize(5, rootH);
    144 		m_resizerBarC.setSize(5, rootH);
    145 
    146 		addAndMakeVisible(m_resizerBarA);
    147 		addAndMakeVisible(m_resizerBarB);
    148 		addAndMakeVisible(m_resizerBarC);
    149 
    150 		PatchManager::resized();
    151 
    152 		const auto& config = m_editor.getProcessor().getConfig();
    153 
    154 		if(config.getIntValue("pm_layout", static_cast<int>(LayoutType::List)) == static_cast<int>(LayoutType::Grid))
    155 			setLayout(LayoutType::Grid);
    156 		else
    157 			PatchManager::resized();
    158 
    159 		startTimer(200);
    160 	}
    161 
    162 	PatchManager::~PatchManager()
    163 	{
    164 		stopTimer();
    165 
    166 		delete m_status;
    167 		delete m_info;
    168 		delete m_searchList;
    169 		delete m_list;
    170 		delete m_grid;
    171 
    172 		// trees emit onSelectionChanged, be sure to guard it 
    173 		m_list = nullptr;
    174 		m_grid = nullptr;
    175 
    176 		delete m_searchTreeTags;
    177 		delete m_treeTags;
    178 		delete m_searchTreeDS;
    179 		delete m_treeDS;
    180 	}
    181 
    182 	void PatchManager::timerCallback()
    183 	{
    184 		uiProcess();
    185 	}
    186 
    187 	void PatchManager::processDirty(const pluginLib::patchDB::Dirty& _dirty) const
    188 	{
    189 		m_treeDS->processDirty(_dirty);
    190 		m_treeTags->processDirty(_dirty);
    191 		getListModel()->processDirty(_dirty);
    192 
    193 		m_status->setScanning(isScanning());
    194 
    195 		m_info->processDirty(_dirty);
    196 
    197 		if(!_dirty.errors.empty())
    198 		{
    199 			std::string msg = "Patch Manager encountered errors:\n\n";
    200 			for(size_t i=0; i<_dirty.errors.size(); ++i)
    201 			{
    202 				msg += _dirty.errors[i];
    203 				if(i < _dirty.errors.size() - 1)
    204 					msg += "\n";
    205 			}
    206 
    207 			genericUI::MessageBox::showOk(juce::AlertWindow::WarningIcon, "Patch Manager Error", msg);
    208 		}
    209 	}
    210 
    211 	void PatchManager::setSelectedItem(Tree* _tree, const TreeItem* _item)
    212 	{
    213 		m_selectedItems[_tree] = std::set{_item};
    214 
    215 		if(_tree == m_treeDS)
    216 			m_treeTags->onParentSearchChanged(_item->getSearchRequest());
    217 
    218 		onSelectedItemsChanged();
    219 	}
    220 
    221 	void PatchManager::addSelectedItem(Tree* _tree, const TreeItem* _item)
    222 	{
    223 		const auto oldCount = m_selectedItems[_tree].size();
    224 		m_selectedItems[_tree].insert(_item);
    225 		const auto newCount = m_selectedItems[_tree].size();
    226 		if(newCount > oldCount)
    227 			onSelectedItemsChanged();
    228 	}
    229 
    230 	// ReSharper disable once CppParameterMayBeConstPtrOrRef - wrong
    231 	void PatchManager::removeSelectedItem(Tree* _tree, const TreeItem* _item)
    232 	{
    233 		const auto it = m_selectedItems.find(_tree);
    234 		if(it == m_selectedItems.end())
    235 			return;
    236 		if(!it->second.erase(_item))
    237 			return;
    238 		onSelectedItemsChanged();
    239 	}
    240 
    241 	bool PatchManager::setSelectedPatch(const pluginLib::patchDB::PatchPtr& _patch, const pluginLib::patchDB::SearchHandle _fromSearch)
    242 	{
    243 		return setSelectedPatch(getCurrentPart(), _patch, _fromSearch);
    244 	}
    245 
    246 	void PatchManager::setLayout(const LayoutType _layout)
    247 	{
    248 		if(m_layout == _layout)
    249 			return;
    250 
    251 		auto* oldModel = getListModel();
    252 
    253 		m_layout = _layout;
    254 
    255 		auto* newModel = getListModel();
    256 
    257 		m_searchList->setListModel(newModel);
    258 
    259 		newModel->setContent(oldModel->getSearchHandle());
    260 		newModel->setSelectedEntries(oldModel->getSelectedEntries());
    261 
    262 		dynamic_cast<Component*>(newModel)->setVisible(true);
    263 		dynamic_cast<Component*>(oldModel)->setVisible(false);
    264 
    265 		if(m_firstTimeGridLayout && _layout == LayoutType::Grid)
    266 		{
    267 			m_firstTimeGridLayout = false;
    268 			setGridLayout128();
    269 		}
    270 		else
    271 		{
    272 			resized();
    273 		}
    274 
    275 		auto& config = m_editor.getProcessor().getConfig();
    276 		config.setValue("pm_layout", static_cast<int>(_layout));
    277 		config.saveIfNeeded();
    278 	}
    279 
    280 	bool PatchManager::setGridLayout128()
    281 	{
    282 		if(m_layout != LayoutType::Grid)
    283 			return false;
    284 
    285 		const auto columnCount = 128.0f / static_cast<float>(m_grid->getSuggestedItemsPerRow());
    286 
    287 		const auto pos = static_cast<int>(static_cast<float>(getWidth()) - m_grid->getItemWidth() * columnCount - static_cast<float>(m_resizerBarB.getWidth()));
    288 
    289 		m_stretchableManager.setItemPosition(3, pos - 1);	// prevent rounding issues
    290 
    291 		resized();
    292 
    293 		return true;
    294 	}
    295 
    296 	void PatchManager::setCustomSearch(const pluginLib::patchDB::SearchHandle _sh) const
    297 	{
    298 		m_treeDS->clearSelectedItems();
    299 		m_treeTags->clearSelectedItems();
    300 
    301 		getListModel()->setContent(_sh);
    302 	}
    303 
    304 	void PatchManager::bringToFront() const
    305 	{
    306 		m_editor.selectTabWithComponent(this);
    307 	}
    308 
    309 	bool PatchManager::selectPatch(const uint32_t _part, const int _offset)
    310 	{
    311 		auto [patch, _] = m_state.getNeighbourPreset(_part, _offset);
    312 
    313 		if(!patch)
    314 			return false;
    315 
    316 		if(!setSelectedPatch(_part, patch, m_state.getSearchHandle(_part)))
    317 			return false;
    318 
    319 		if(_part == getCurrentPart())
    320 			getListModel()->setSelectedPatches({patch});
    321 
    322 		return true;
    323 	}
    324 
    325 	bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch, pluginLib::patchDB::SearchHandle _fromSearch)
    326 	{
    327 		if(!activatePatch(_patch, _part))
    328 			return false;
    329 
    330 		m_state.setSelectedPatch(_part, pluginLib::patchDB::PatchKey(*_patch), _fromSearch);
    331 
    332 		if(_part == getCurrentPart())
    333 			m_info->setPatch(_patch);
    334 
    335 		return true;
    336 	}
    337 
    338 	bool PatchManager::setSelectedDataSource(const pluginLib::patchDB::DataSourceNodePtr& _ds) const
    339 	{
    340 		if(auto* item = m_treeDS->getItem(*_ds))
    341 		{
    342 			selectTreeItem(item);
    343 			return true;
    344 		}
    345 		return false;
    346 	}
    347 
    348 	pluginLib::patchDB::DataSourceNodePtr PatchManager::getSelectedDataSource() const
    349 	{
    350 		const auto* item = dynamic_cast<DatasourceTreeItem*>(m_treeDS->getSelectedItem(0));
    351 		if(!item)
    352 			return {};
    353 		return item->getDataSource();
    354 	}
    355 
    356 	TreeItem* PatchManager::getSelectedDataSourceTreeItem() const
    357 	{
    358 		if (!m_treeDS)
    359 			return nullptr;
    360 		auto ds = getSelectedDataSource();
    361 		if (!ds)
    362 			return nullptr;
    363 		return m_treeDS->getItem(*ds);
    364 	}
    365 
    366 	bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch)
    367 	{
    368 		if(!isValid(_patch))
    369 			return false;
    370 
    371 		const auto patchDs = _patch->source.lock();
    372 
    373 		if(!patchDs)
    374 			return false;
    375 
    376 		if(!setSelectedPatch(_part, pluginLib::patchDB::PatchKey(*_patch)))
    377 			return false;
    378 
    379 		return true;
    380 	}
    381 
    382 	bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchKey& _patch)
    383 	{
    384 		// we've got a patch, but we do not know its search handle, i.e. which list it is part of, find the missing information
    385 
    386 		if(!_patch.isValid())
    387 			return false;
    388 
    389 		const auto searchHandle = getSearchHandle(*_patch.source, _part == getCurrentPart());
    390 
    391 		if(searchHandle == pluginLib::patchDB::g_invalidSearchHandle)
    392 			return false;
    393 
    394 		m_state.setSelectedPatch(_part, _patch, searchHandle);
    395 
    396 		if(getCurrentPart() == _part)
    397 			getListModel()->setSelectedPatches({_patch});
    398 
    399 		onSelectedPatchChanged(_part, _patch);
    400 
    401 		return true;
    402 	}
    403 
    404 	void PatchManager::copyPatchesToLocalStorage(const pluginLib::patchDB::DataSourceNodePtr& _ds, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, int _part)
    405 	{
    406 		copyPatchesTo(_ds, _patches, -1, [this, _part](const std::vector<pluginLib::patchDB::PatchPtr>& _savedPatches)
    407 		{
    408 			if(_part == -1)
    409 				return;
    410 
    411 			juce::MessageManager::callAsync([this, _part, _savedPatches]
    412 			{
    413 				setSelectedPatch(_part, _savedPatches.front());
    414 			});
    415 		});
    416 	}
    417 
    418 	uint32_t PatchManager::createSaveMenuEntries(juce::PopupMenu& _menu, uint32_t _part, const std::string& _name/* = "patch"*/, uint64_t _userData/* = 0*/)
    419 	{
    420 		const auto& state = getState();
    421 		const auto key = state.getPatch(_part);
    422 
    423 		uint32_t countAdded = 0;
    424 
    425 		if(key.isValid() && key.source->type == pluginLib::patchDB::SourceType::LocalStorage)
    426 		{
    427 			// the key that is stored in the state might not contain patches, find the real data source in the DB
    428 			const auto ds = getDataSource(*key.source);
    429 
    430 			if(ds)
    431 			{
    432 				if(const auto p = ds->getPatch(key))
    433 				{
    434 					if(*p == key)
    435 					{
    436 						++countAdded;
    437 						_menu.addItem("Overwrite " + _name + " '" + p->getName() + "' in user bank '" + ds->name + "'", true, false, [this, p, _part, _userData]
    438 						{
    439 							const auto newPatch = requestPatchForPart(_part, _userData);
    440 							if(newPatch)
    441 							{
    442 								replacePatch(p, newPatch);
    443 							}
    444 						});
    445 					}
    446 				}
    447 			}
    448 		}
    449 
    450 		const auto existingLocalDS = getDataSourcesOfSourceType(pluginLib::patchDB::SourceType::LocalStorage);
    451 
    452 		if(!existingLocalDS.empty())
    453 		{
    454 			if(countAdded)
    455 				_menu.addSeparator();
    456 
    457 			for (const auto& ds : existingLocalDS)
    458 			{
    459 				++countAdded;
    460 				_menu.addItem("Add " + _name + " to user bank '" + ds->name + "'", true, false, [this, ds, _part, _userData]
    461 				{
    462 					const auto newPatch = requestPatchForPart(_part, _userData);
    463 
    464 					if(!newPatch)
    465 						return;
    466 
    467 					copyPatchesToLocalStorage(ds, {newPatch}, static_cast<int>(_part));
    468 				});
    469 			}
    470 		}
    471 		else
    472 		{
    473 			++countAdded;
    474 			_menu.addItem("Create new user bank and add " + _name, true, false, [this, _part, _userData]
    475 			{
    476 				const auto newPatch = requestPatchForPart(_part, _userData);
    477 
    478 				if(!newPatch)
    479 					return;
    480 
    481 				pluginLib::patchDB::DataSource ds;
    482 
    483 				ds.name = "User Bank";
    484 				ds.type = pluginLib::patchDB::SourceType::LocalStorage;
    485 				ds.origin = pluginLib::patchDB::DataSourceOrigin::Manual;
    486 				ds.timestamp = std::chrono::system_clock::now();
    487 				addDataSource(ds, false, [newPatch, _part, this](const bool _success, const std::shared_ptr<pluginLib::patchDB::DataSourceNode>& _ds)
    488 				{
    489 					if(_success)
    490 						copyPatchesToLocalStorage(_ds, {newPatch}, static_cast<int>(_part));
    491 				});
    492 			});
    493 		}
    494 
    495 		return countAdded;
    496 	}
    497 
    498 	std::string PatchManager::getTagTypeName(const pluginLib::patchDB::TagType _type) const
    499 	{
    500 		const auto it = m_tagTypeNames.find(_type);
    501 		if(it == m_tagTypeNames.end())
    502 		{
    503 			return {};
    504 		}
    505 		return it->second;
    506 	}
    507 
    508 	void PatchManager::setTagTypeName(const pluginLib::patchDB::TagType _type, const std::string& _name)
    509 	{
    510 		if(_name.empty())
    511 		{
    512 			m_tagTypeNames.erase(_type);
    513 			return;
    514 		}
    515 
    516 		m_tagTypeNames[_type] = _name;
    517 	}
    518 
    519 	bool PatchManager::selectPrevPreset(const uint32_t _part)
    520 	{
    521 		return selectPatch(_part, -1);
    522 	}
    523 
    524 	bool PatchManager::selectNextPreset(const uint32_t _part)
    525 	{
    526 		return selectPatch(_part, 1);
    527 	}
    528 
    529 	bool PatchManager::selectPatch(const uint32_t _part, const pluginLib::patchDB::DataSource& _ds, const uint32_t _program)
    530 	{
    531 		const auto searchHandle = getSearchHandle(_ds, _part == getCurrentPart());
    532 
    533 		if(searchHandle == pluginLib::patchDB::g_invalidSearchHandle)
    534 			return false;
    535 
    536 		const auto s = getSearch(searchHandle);
    537 		if(!s)
    538 			return false;
    539 
    540 		pluginLib::patchDB::PatchPtr p;
    541 
    542 		std::shared_lock lockResults(s->resultsMutex);
    543 		for (const auto& patch : s->results)
    544 		{
    545 			if(patch->program == _program)
    546 			{
    547 				p = patch;
    548 				break;
    549 			}
    550 		}
    551 
    552 		if(!p)
    553 			return false;
    554 
    555 		if(!activatePatch(p, _part))
    556 			return false;
    557 
    558 		setSelectedPatch(_part, p, s->handle);
    559 
    560 		if(_part == getCurrentPart())
    561 			getListModel()->setSelectedPatches({p});
    562 
    563 		return true;
    564 	}
    565 
    566 	void PatchManager::setListStatus(uint32_t _selected, uint32_t _total) const
    567 	{
    568 		m_status->setListStatus(_selected, _total);
    569 	}
    570 
    571 	pluginLib::patchDB::Color PatchManager::getPatchColor(const pluginLib::patchDB::PatchPtr& _patch) const
    572 	{
    573 		// we want to prevent that a whole list is colored with one color just because that list is based on a tag, prefer other tags instead
    574 		pluginLib::patchDB::TypedTags ignoreTags;
    575 
    576 		for (const auto& selectedItem : m_selectedItems)
    577 		{
    578 			for (const auto& item : selectedItem.second)
    579 			{
    580 				const auto& s = item->getSearchRequest();
    581 				ignoreTags.add(s.tags);
    582 			}
    583 		}
    584 		return DB::getPatchColor(_patch, ignoreTags);
    585 	}
    586 
    587 	bool PatchManager::addGroupTreeItemForTag(const pluginLib::patchDB::TagType _type) const
    588 	{
    589 		return addGroupTreeItemForTag(_type, getTagTypeName(_type));
    590 	}
    591 
    592 	bool PatchManager::addGroupTreeItemForTag(const pluginLib::patchDB::TagType _type, const std::string& _name) const
    593 	{
    594 		const auto groupType = toGroupType(_type);
    595 		if(groupType == GroupType::Invalid)
    596 			return false;
    597 		if(_name.empty())
    598 			return false;
    599 		if(m_treeTags->getItem(groupType))
    600 			return false;
    601 		m_treeTags->addGroup(groupType, _name);
    602 		return true;
    603 	}
    604 
    605 	void PatchManager::paint(juce::Graphics& g)
    606 	{
    607 		g.fillAll(juce::Colour(0,0,0));
    608 	}
    609 
    610 	void PatchManager::exportPresets(const juce::File& _file, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, const pluginLib::FileType& _fileType) const
    611 	{
    612 #if SYNTHLIB_DEMO_MODE
    613 		getEditor().showDemoRestrictionMessageBox();
    614 #else
    615 		pluginLib::FileType type = _fileType;
    616 		const auto name = Editor::createValidFilename(type, _file);
    617 
    618 		std::vector<pluginLib::patchDB::Data> patchData;
    619 		for (const auto& patch : _patches)
    620 		{
    621 			const auto patchSysex = applyModifications(patch, type, pluginLib::ExportType::File);
    622 
    623 			if(!patchSysex.empty())
    624 				patchData.push_back(patchSysex);
    625 		}
    626 
    627 		if(!Editor::savePresets(type, name, patchData))
    628 			genericUI::MessageBox::showOk(juce::AlertWindow::WarningIcon, "Save failed", "Failed to write data to " + _file.getFullPathName().toStdString());
    629 #endif
    630 	}
    631 
    632 	bool PatchManager::exportPresets(std::vector<pluginLib::patchDB::PatchPtr>&& _patches, const pluginLib::FileType& _fileType) const
    633 	{
    634 		const auto patchCount = _patches.size();
    635 
    636 		auto exportPatches = [p = std::move(_patches), this, _fileType]
    637 		{
    638 			auto patches = p;
    639 			ListModel::sortPatches(patches, pluginLib::patchDB::SourceType::LocalStorage);
    640 			getEditor().savePreset(_fileType, [this, p = std::move(patches), _fileType](const juce::File& _file)
    641 			{
    642 				exportPresets(_file, p, _fileType);
    643 			});
    644 		};
    645 
    646 		if(patchCount > 128)
    647 		{
    648 			genericUI::MessageBox::showOkCancel(
    649 				juce::MessageBoxIconType::WarningIcon,
    650 				"Patch Manager", 
    651 				"You are trying to export more than 128 presets into a single file. Note that this dump exceeds the size of one bank and may not be compatible with your hardware",
    652 				[this, exportPatches](const genericUI::MessageBox::Result _result)
    653 				{
    654 					if (_result != genericUI::MessageBox::Result::Ok)
    655 						return;
    656 
    657 					exportPatches();
    658 				});
    659 		}
    660 		else
    661 		{
    662 			exportPatches();
    663 		}
    664 
    665 		return true;
    666 	}
    667 
    668 	void PatchManager::resized()
    669 	{
    670 		if(!m_treeDS)
    671 			return;
    672 
    673 		m_info->setVisible(m_layout == LayoutType::List);
    674 		m_resizerBarC.setVisible(m_layout == LayoutType::List);
    675 
    676 		std::vector<Component*> comps = {m_treeDS, &m_resizerBarA, m_treeTags, &m_resizerBarB, dynamic_cast<juce::Component*>(getListModel())};
    677 		if(m_layout == LayoutType::List)
    678 		{
    679 			comps.push_back(&m_resizerBarC);
    680 			comps.push_back(m_info);
    681 		}
    682 
    683 		m_stretchableManager.layOutComponents(comps.data(), static_cast<int>(comps.size()), 0, 0, getWidth(), getHeight(), false, false);
    684 
    685 		auto layoutXAxis = [](Component* _target, const Component* _source)
    686 		{
    687 			_target->setTopLeftPosition(_source->getX(), _target->getY());
    688 			_target->setSize(_source->getWidth(), _target->getHeight());
    689 		};
    690 
    691 		layoutXAxis(m_searchTreeDS, m_treeDS);
    692 		layoutXAxis(m_searchTreeTags, m_treeTags);
    693 
    694 		if(m_layout == LayoutType::List)
    695 		{
    696 			layoutXAxis(m_searchList, dynamic_cast<Component*>(getListModel()));
    697 			layoutXAxis(m_status, m_info);
    698 		}
    699 		else
    700 		{
    701 			m_searchList->setTopLeftPosition(m_grid->getX(), m_searchList->getY());
    702 			m_searchList->setSize(m_grid->getWidth()/3 - g_padding, m_searchList->getHeight());
    703 
    704 			m_status->setTopLeftPosition(m_searchList->getX() + m_searchList->getWidth() + g_padding, m_searchList->getY());
    705 			m_status->setSize(m_grid->getWidth()/3*2, m_status->getHeight());
    706 		}
    707 	}
    708 
    709 	juce::Colour PatchManager::getResizerBarColor() const
    710 	{
    711 		return m_treeDS->findColour(juce::TreeView::ColourIds::selectedItemBackgroundColourId);
    712 	}
    713 
    714 	bool PatchManager::copyPart(const uint8_t _target, const uint8_t _source)
    715 	{
    716 		if(_target == _source)
    717 			return false;
    718 
    719 		const auto source = requestPatchForPart(_source);
    720 		if(!source)
    721 			return false;
    722 
    723 		if(!activatePatch(source, _target))
    724 			return false;
    725 
    726 		m_state.copy(_target, _source);
    727 
    728 		if(getCurrentPart() == _target)
    729 			setSelectedPatch(_target, m_state.getPatch(_target));
    730 
    731 		return true;
    732 	}
    733 
    734 	std::shared_ptr<genericUI::UiObject> PatchManager::getTemplate(const std::string& _name) const
    735 	{
    736 		return m_editor.getTemplate(_name);
    737 	}
    738 
    739 	bool PatchManager::activatePatch(const std::string& _filename, const uint32_t _part)
    740 	{
    741 		if(_part >= m_state.getPartCount() || _part > m_editor.getProcessor().getController().getPartCount())
    742 			return false;
    743 
    744 		const auto patches = loadPatchesFromFiles(std::vector<std::string>{_filename});
    745 
    746 		if(patches.empty())
    747 			return false;
    748 
    749 		const auto& patch = patches.front();
    750 
    751 		if(!activatePatch(patch, _part))
    752 			return false;
    753 
    754 		if(getCurrentPart() == _part)
    755 			getListModel()->setSelectedPatches(std::set<pluginLib::patchDB::PatchKey>{});
    756 
    757 		return true;
    758 	}
    759 
    760 	std::vector<pluginLib::patchDB::PatchPtr> PatchManager::loadPatchesFromFiles(const juce::StringArray& _files)
    761 	{
    762 		std::vector<std::string> files;
    763 
    764 		for (const auto& file : _files)
    765 			files.push_back(file.toStdString());
    766 
    767 		return loadPatchesFromFiles(files);
    768 	}
    769 
    770 	std::vector<pluginLib::patchDB::PatchPtr> PatchManager::loadPatchesFromFiles(const std::vector<std::string>& _files)
    771 	{
    772 		std::vector<pluginLib::patchDB::PatchPtr> patches;
    773 
    774 		for (const auto& file : _files)
    775 		{
    776 			pluginLib::patchDB::DataList results;
    777 			if(!loadFile(results, file) || results.empty())
    778 				continue;
    779 
    780 			const auto defaultName = results.size() == 1 ? baseLib::filesystem::stripExtension(baseLib::filesystem::getFilenameWithoutPath(file)) : "";
    781 
    782 			for (auto& result : results)
    783 			{
    784 				if(const auto patch = initializePatch(std::move(result), defaultName))
    785 					patches.push_back(patch);
    786 			}
    787 		}
    788 		return patches;
    789 	}
    790 
    791 	void PatchManager::onLoadFinished()
    792 	{
    793 		DB::onLoadFinished();
    794 
    795 		for(uint32_t i=0; i<std::min(m_editor.getProcessor().getController().getPartCount(), static_cast<uint8_t>(m_state.getPartCount())); ++i)
    796 		{
    797 			const auto p = m_state.getPatch(i);
    798 
    799 			// If the state has been deserialized, the patch key is valid but the search handle is not. Only restore if that is the case
    800 			if(p.isValid() && m_state.getSearchHandle(i) == pluginLib::patchDB::g_invalidSearchHandle)
    801 			{
    802 				if(!setSelectedPatch(i, p))
    803 					m_state.clear(i);
    804 			}
    805 			else if(!m_state.isValid(i))
    806 			{
    807 				// otherwise, try to restore from the currently loaded patch
    808 				updateStateAsync(i, requestPatchForPart(i));
    809 			}
    810 		}
    811 	}
    812 
    813 	void PatchManager::setPerInstanceConfig(const std::vector<uint8_t>& _data)
    814 	{
    815 		if(_data.empty())
    816 			return;
    817 		try
    818 		{
    819 			pluginLib::PluginStream s(_data);
    820 			const auto version = s.read<uint32_t>();
    821 			if(version != 1)
    822 				return;
    823 			m_state.setConfig(s);
    824 		}
    825 		catch(std::range_error& e)
    826 		{
    827 			LOG("Failed to to load per instance config: " << e.what());
    828 		}
    829 	}
    830 
    831 	void PatchManager::getPerInstanceConfig(std::vector<uint8_t>& _data) const
    832 	{
    833 		pluginLib::PluginStream s;
    834 		s.write<uint32_t>(1);	// version
    835 		m_state.getConfig(s);
    836 		s.toVector(_data);
    837 	}
    838 
    839 	void PatchManager::onProgramChanged(const uint32_t _part)
    840 	{
    841 		if(isLoading())
    842 			return;
    843 		return;
    844 		pluginLib::patchDB::Data data;
    845 		if(!requestPatchForPart(data, _part, 0))
    846 			return;
    847 		const auto patch = initializePatch(std::move(data), {});
    848 		if(!patch)
    849 			return;
    850 		updateStateAsync(_part, patch);
    851 	}
    852 
    853 	void PatchManager::setCurrentPart(uint32_t _part)
    854 	{
    855 		if(!m_state.isValid(_part))
    856 			return;
    857 
    858 		setSelectedPatch(_part, m_state.getPatch(_part));
    859 	}
    860 
    861 	void PatchManager::updateStateAsync(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch)
    862 	{
    863 		if(!isValid(_patch))
    864 			return;
    865 
    866 		const auto patchDs = _patch->source.lock();
    867 
    868 		if(patchDs)
    869 		{
    870 			setSelectedPatch(_part, _patch);
    871 			return;
    872 		}
    873 
    874 		// we've got a patch, but we do not know its datasource and search handle, find the data source by executing a search
    875 
    876 		findDatasourceForPatch(_patch, [this, _part](const pluginLib::patchDB::Search& _search)
    877 		{
    878 			const auto handle = _search.handle;
    879 
    880 			std::vector<pluginLib::patchDB::PatchPtr> results;
    881 			results.assign(_search.results.begin(), _search.results.end());
    882 
    883 			if(results.empty())
    884 				return;
    885 
    886 			if(results.size() > 1)
    887 			{
    888 				// if there are multiple results, sort them, we prefer ROM results over other results
    889 
    890 				std::sort(results.begin(), results.end(), [](const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b)
    891 				{
    892 					const auto dsA = _a->source.lock();
    893 					const auto dsB = _b->source.lock();
    894 
    895 					if(!dsA || !dsB)
    896 						return true;
    897 
    898 					if(dsA->type < dsB->type)
    899 						return true;
    900 					if(dsA->type > dsB->type)
    901 						return false;
    902 					if(dsA->name < dsB->name)
    903 						return true;
    904 					if(dsA->name > dsB->name)
    905 						return false;
    906 					if(_a->program < _b->program)
    907 						return true;
    908 					return false;
    909 				});
    910 			}
    911 
    912 			const auto currentPatch = results.front();
    913 
    914 			const auto key = pluginLib::patchDB::PatchKey(*currentPatch);
    915 
    916 			runOnUiThread([this, _part, key, handle]
    917 			{
    918 				cancelSearch(handle);
    919 				setSelectedPatch(_part, key);
    920 			});
    921 		});
    922 	}
    923 
    924 	ListModel* PatchManager::getListModel() const
    925 	{
    926 		if(m_layout == LayoutType::List)
    927 			return m_list;
    928 		return m_grid;
    929 	}
    930 
    931 	void PatchManager::startLoaderThread(const juce::File& _migrateFromDir/* = {}*/)
    932 	{
    933 		if(_migrateFromDir.getFullPathName().isEmpty())
    934 		{
    935 			const auto& configOptions = m_editor.getProcessor().getConfigOptions();
    936 			DB::startLoaderThread(configOptions.getDefaultFile().getParentDirectory());
    937 			return;
    938 		}
    939 		DB::startLoaderThread(_migrateFromDir);
    940 	}
    941 
    942 	pluginLib::patchDB::SearchHandle PatchManager::getSearchHandle(const pluginLib::patchDB::DataSource& _ds, bool _selectTreeItem)
    943 	{
    944 		if(auto* item = m_treeDS->getItem(_ds))
    945 		{
    946 			const auto searchHandle = item->getSearchHandle();
    947 
    948 			// select the tree item that contains the data source and expand all parents to make it visible
    949 			if(_selectTreeItem)
    950 			{
    951 				selectTreeItem(item);
    952 			}
    953 
    954 			return searchHandle;
    955 		}
    956 
    957 		const auto search = getSearch(_ds);
    958 
    959 		if(!search)
    960 			return pluginLib::patchDB::g_invalidSearchHandle;
    961 
    962 		return search->handle;
    963 	}
    964 
    965 	void PatchManager::onSelectedItemsChanged()
    966 	{
    967 		// trees emit onSelectionChanged in destructor, be sure to guard it 
    968 		if(!getListModel())
    969 			return;
    970 
    971 		const auto selectedTags = m_selectedItems[m_treeTags];
    972 
    973 		auto selectItem = [&](const TreeItem* _item)
    974 		{
    975 			if(_item->getSearchHandle() != pluginLib::patchDB::g_invalidSearchHandle)
    976 			{
    977 				getListModel()->setContent(_item->getSearchHandle());
    978 				return true;
    979 			}
    980 			return false;
    981 		};
    982 
    983 		if(!selectedTags.empty())
    984 		{
    985 			if(selectedTags.size() == 1)
    986 			{
    987 				if(selectItem(*selectedTags.begin()))
    988 					return;
    989 			}
    990 			else
    991 			{
    992 				pluginLib::patchDB::SearchRequest search = (*selectedTags.begin())->getSearchRequest();
    993 				for (const auto& selectedTag : selectedTags)
    994 					search.tags.add(selectedTag->getSearchRequest().tags);
    995 				getListModel()->setContent(std::move(search));
    996 				return;
    997 			}
    998 		}
    999 
   1000 		const auto selectedDataSources = m_selectedItems[m_treeDS];
   1001 
   1002 		if(!selectedDataSources.empty())
   1003 		{
   1004 			const auto* item = *selectedDataSources.begin();
   1005 			selectItem(item);
   1006 		}
   1007 	}
   1008 
   1009 	void PatchManager::changeListenerCallback(juce::ChangeBroadcaster* _source)
   1010 	{
   1011 		auto* cs = dynamic_cast<juce::ColourSelector*>(_source);
   1012 
   1013 		if(cs)
   1014 		{
   1015 			const auto tagType = static_cast<pluginLib::patchDB::TagType>(static_cast<int>(cs->getProperties()["tagType"]));
   1016 			const auto tag = cs->getProperties()["tag"].toString().toStdString();
   1017 
   1018 			if(tagType != pluginLib::patchDB::TagType::Invalid && !tag.empty())
   1019 			{
   1020 				const auto color = cs->getCurrentColour();
   1021 				setTagColor(tagType, tag, color.getARGB());
   1022 
   1023 				repaint();
   1024 			}
   1025 		}
   1026 	}
   1027 
   1028 	void PatchManager::selectTreeItem(TreeItem* _item)
   1029 	{
   1030 		if(!_item)
   1031 			return;
   1032 
   1033 		_item->setSelected(true, true);
   1034 
   1035 		auto* parent = _item->getParentItem();
   1036 		while(parent)
   1037 		{
   1038 			parent->setOpen(true);
   1039 			parent = parent->getParentItem();
   1040 		}
   1041 
   1042 		_item->getOwnerView()->scrollToKeepItemVisible(_item);
   1043 	}
   1044 
   1045 	std::vector<pluginLib::patchDB::PatchPtr> PatchManager::getPatchesFromString(const std::string& _text)
   1046 	{
   1047 		auto data = pluginLib::Clipboard::getDataFromString(m_editor.getProcessor(), _text);
   1048 
   1049 		if(data.sysex.empty())
   1050 			return {};
   1051 
   1052 		pluginLib::patchDB::DataList results;
   1053 
   1054 		if (!parseFileData(results, data.sysex))
   1055 			return {};
   1056 
   1057 		std::vector<pluginLib::patchDB::PatchPtr> patches;
   1058 
   1059 		for (auto& result : results)
   1060 		{
   1061 			if(const auto patch = initializePatch(std::move(result), {}))
   1062 				patches.push_back(patch);
   1063 		}
   1064 
   1065 		return patches;
   1066 	}
   1067 
   1068 	std::vector<pluginLib::patchDB::PatchPtr> PatchManager::getPatchesFromClipboard()
   1069 	{
   1070 		return getPatchesFromString(juce::SystemClipboard::getTextFromClipboard().toStdString());
   1071 	}
   1072 
   1073 	bool PatchManager::activatePatchFromString(const std::string& _text)
   1074 	{
   1075 		const auto patches = getPatchesFromString(_text);
   1076 
   1077 		if(patches.size() != 1)
   1078 			return false;
   1079 
   1080 		return activatePatch(patches.front(), getCurrentPart());
   1081 	}
   1082 
   1083 	bool PatchManager::activatePatchFromClipboard()
   1084 	{
   1085 		return activatePatchFromString(juce::SystemClipboard::getTextFromClipboard().toStdString());
   1086 	}
   1087 
   1088 	std::string PatchManager::toString(const pluginLib::patchDB::PatchPtr& _patch, const pluginLib::FileType& _fileType, const pluginLib::ExportType _exportType) const
   1089 	{
   1090 		if(!_patch)
   1091 			return {};
   1092 
   1093 		const auto data = applyModifications(_patch, _fileType, _exportType);
   1094 
   1095 		return pluginLib::Clipboard::createJsonString(m_editor.getProcessor(), {}, {}, data);
   1096 	}
   1097 }