diff --git a/hi_backend/backend/ai_tools/RestServer.cpp b/hi_backend/backend/ai_tools/RestServer.cpp index eacbfc110d..2c34041796 100644 --- a/hi_backend/backend/ai_tools/RestServer.cpp +++ b/hi_backend/backend/ai_tools/RestServer.cpp @@ -41,6 +41,13 @@ #undef Rectangle #endif +// On Linux, resolv.h (included transitively by httplib.h) defines DELETE as +// ns_uop_delete (= 0), which collides with the RestServer::Method::DELETE enum +// value in switch statements. Undefine it here after all system headers are done. +#ifdef DELETE +#undef DELETE +#endif + namespace hise { using namespace juce; //============================================================================== diff --git a/hi_core/hi_components/floating_layout/FrontendPanelTypes.cpp b/hi_core/hi_components/floating_layout/FrontendPanelTypes.cpp index 754ef07cf7..a47c809d31 100644 --- a/hi_core/hi_components/floating_layout/FrontendPanelTypes.cpp +++ b/hi_core/hi_components/floating_layout/FrontendPanelTypes.cpp @@ -996,6 +996,7 @@ var PresetBrowserPanel::toDynamicObject() const storePropertyInObject(obj, SpecialPanelIds::ShowSaveButton, options.showSaveButtons); storePropertyInObject(obj, SpecialPanelIds::ShowExpansionsAsColumn, options.showExpansions); + storePropertyInObject(obj, SpecialPanelIds::ShowExpansionEditButtons, options.showExpansionEditButtons); storePropertyInObject(obj, SpecialPanelIds::ShowFolderButton, options.showFolderButton); storePropertyInObject(obj, SpecialPanelIds::ShowNotes, options.showNotesLabel); storePropertyInObject(obj, SpecialPanelIds::ShowEditButtons, options.showEditButtons); @@ -1007,6 +1008,7 @@ var PresetBrowserPanel::toDynamicObject() const storePropertyInObject(obj, SpecialPanelIds::FavoriteIconOffset, options.favoriteIconOffset); storePropertyInObject(obj, SpecialPanelIds::ShowFavoriteIcon, options.showFavoriteIcons); storePropertyInObject(obj, SpecialPanelIds::FullPathFavorites, options.fullPathFavorites); + storePropertyInObject(obj, SpecialPanelIds::FullPathSearch, options.fullPathSearch); storePropertyInObject(obj, SpecialPanelIds::ButtonsInsideBorder, options.buttonsInsideBorder); storePropertyInObject(obj, SpecialPanelIds::NumColumns, options.numColumns); storePropertyInObject(obj, SpecialPanelIds::ColumnWidthRatio, var(options.columnWidthRatios)); @@ -1034,9 +1036,11 @@ void PresetBrowserPanel::fromDynamicObject(const var& object) options.showSearchBar = getPropertyWithDefault(object, SpecialPanelIds::ShowSearchBar); options.favoriteIconOffset = getPropertyWithDefault(object, SpecialPanelIds::FavoriteIconOffset); options.fullPathFavorites = getPropertyWithDefault(object, SpecialPanelIds::FullPathFavorites); + options.fullPathSearch = getPropertyWithDefault(object, SpecialPanelIds::FullPathSearch); options.buttonsInsideBorder = getPropertyWithDefault(object, SpecialPanelIds::ButtonsInsideBorder); options.editButtonOffset = getPropertyWithDefault(object, SpecialPanelIds::EditButtonOffset); options.showExpansions = getPropertyWithDefault(object, SpecialPanelIds::ShowExpansionsAsColumn); + options.showExpansionEditButtons = getPropertyWithDefault(object, SpecialPanelIds::ShowExpansionEditButtons); options.numColumns = getPropertyWithDefault(object, SpecialPanelIds::NumColumns); auto ratios = getPropertyWithDefault(object, SpecialPanelIds::ColumnWidthRatio); @@ -1128,6 +1132,7 @@ juce::Identifier PresetBrowserPanel::getDefaultablePropertyId(int index) const RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::ColumnRowPadding, "ColumnRowPadding"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::SearchBarBounds, "SearchBarBounds"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::FullPathFavorites, "FullPathFavorites"); + RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::FullPathSearch, "FullPathSearch"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::FavoriteButtonBounds, "FavoriteButtonBounds"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::SaveButtonBounds, "SaveButtonBounds"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::MoreButtonBounds, "MoreButtonBounds"); @@ -1136,6 +1141,7 @@ juce::Identifier PresetBrowserPanel::getDefaultablePropertyId(int index) const RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::ShowExpansionsAsColumn, "ShowExpansionsAsColumn"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::ShowFavoriteIcon, "ShowFavoriteIcon"); RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::FavoriteIconOffset, "FavoriteIconOffset"); + RETURN_DEFAULT_PROPERTY_ID(index, SpecialPanelIds::ShowExpansionEditButtons, "ShowExpansionEditButtons"); return Identifier(); } @@ -1157,6 +1163,7 @@ var PresetBrowserPanel::getDefaultProperty(int index) const RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowAddButton, true); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowRenameButton, true); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::FullPathFavorites, false); + RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::FullPathSearch, false); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::FavoriteIconOffset, 0); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowDeleteButton, true); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowSearchBar, true); @@ -1169,6 +1176,7 @@ var PresetBrowserPanel::getDefaultProperty(int index) const RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ColumnWidthRatio, var(defaultRatios)); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowExpansionsAsColumn, false); + RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowExpansionEditButtons, false); RETURN_DEFAULT_PROPERTY(index, SpecialPanelIds::ShowFavoriteIcon, true); Array defaultListAreaOffset = {0, 0, 0, 0}; diff --git a/hi_core/hi_components/floating_layout/FrontendPanelTypes.h b/hi_core/hi_components/floating_layout/FrontendPanelTypes.h index 7138a9ac71..8b4bf74c6a 100644 --- a/hi_core/hi_components/floating_layout/FrontendPanelTypes.h +++ b/hi_core/hi_components/floating_layout/FrontendPanelTypes.h @@ -505,7 +505,9 @@ class PresetBrowserPanel : public FloatingTileContent, MoreButtonBounds, FavoriteButtonBounds, FullPathFavorites, + FullPathSearch, FavoriteIconOffset, + ShowExpansionEditButtons, numSpecialProperties }; diff --git a/hi_core/hi_components/plugin_components/PresetBrowser.cpp b/hi_core/hi_components/plugin_components/PresetBrowser.cpp index b4ffbe0cac..f294725c0d 100644 --- a/hi_core/hi_components/plugin_components/PresetBrowser.cpp +++ b/hi_core/hi_components/plugin_components/PresetBrowser.cpp @@ -389,6 +389,9 @@ void PresetBrowser::ModalWindow::buttonClicked(Button* b) auto note = DataBaseHelpers::getNoteFromXml(le.newFile); auto tags = DataBaseHelpers::getTagsFromXml(le.newFile); + if (le.oldFile.getFileName() != "tempFileBeforeMove.preset") + p->updateDatabaseKeyForFileRename(le.oldFile, le.newFile); + le.oldFile.moveFileTo(le.newFile); if (note.isNotEmpty()) @@ -699,6 +702,188 @@ Point PresetBrowser::getMouseHoverInformation() const return p; } +Array PresetBrowser::getAllSearchRoots() const +{ + Array roots; + + if (currentlySelectedExpansion != nullptr) + { + auto userPresetsDir = currentlySelectedExpansion->getSubDirectory(FileHandlerBase::UserPresets); + if (userPresetsDir.isDirectory()) + roots.add(userPresetsDir); + return roots; + } + + if (defaultRoot.isDirectory()) + roots.add(defaultRoot); + + auto& handler = getMainController()->getExpansionHandler(); + + for (int i = 0; i < handler.getNumExpansions(); ++i) + { + if (auto e = handler.getExpansion(i)) + { + auto userPresetsDir = e->getSubDirectory(FileHandlerBase::UserPresets); + if (userPresetsDir.isDirectory() && !roots.contains(userPresetsDir)) + roots.add(userPresetsDir); + } + } + + return roots; +} + +var PresetBrowser::loadDatabaseForRoot(const File& rootDir) const +{ + if (rootDir == rootFile) + return presetDatabase; + + var db = JSON::parse(rootDir.getChildFile("db.json").loadFileAsString()); + return db.isObject() ? db : var(new DynamicObject()); +} + +void PresetBrowser::saveDatabaseForRoot(const var& db, const File& rootDir) +{ + if (rootDir == rootFile) + { + presetDatabase = db; + savePresetDatabase(rootFile); + } + else + { + rootDir.getChildFile("db.json").replaceWithText(JSON::toString(db)); + } +} + +File PresetBrowser::findRootForFile(const File& f) const +{ + if (f.isAChildOf(rootFile)) + return rootFile; + + auto& handler = getMainController()->getExpansionHandler(); + + for (int i = 0; i < handler.getNumExpansions(); ++i) + { + if (auto e = handler.getExpansion(i)) + { + auto userPresetsDir = e->getSubDirectory(FileHandlerBase::UserPresets); + + if (f.isAChildOf(userPresetsDir)) + return userPresetsDir; + } + } + + if (f.isAChildOf(defaultRoot)) + return defaultRoot; + + return {}; +} + +void PresetBrowser::updateDatabaseKeysForRename(const File& oldDirectory, const File& newDirectory) +{ + auto root = findRootForFile(oldDirectory); + + if (!root.isDirectory()) + return; + + var db = loadDatabaseForRoot(root); + DataBaseHelpers::renameEntriesInDatabase(db, root, oldDirectory, newDirectory); + saveDatabaseForRoot(db, root); + invalidateFavoritesCache(); +} + +void PresetBrowser::updateDatabaseKeyForFileRename(const File& oldFile, const File& newFile) +{ + auto root = findRootForFile(oldFile); + + if (!root.isDirectory()) + return; + + var db = loadDatabaseForRoot(root); + DataBaseHelpers::renameFileEntryInDatabase(db, oldFile, newFile); + saveDatabaseForRoot(db, root); + invalidateFavoritesCache(); +} + +void PresetBrowser::rebuildFavoritesCache() const +{ + cachedFavorites.clear(); + + // Build the full cache across ALL roots regardless of which expansion is + // currently selected. getAllFavoritePresets() filters by expansion later. + Array allRoots; + + if (defaultRoot.isDirectory()) + allRoots.add(defaultRoot); + + auto& handler = getMainController()->getExpansionHandler(); + + for (int i = 0; i < handler.getNumExpansions(); ++i) + { + if (auto e = handler.getExpansion(i)) + { + auto userPresetsDir = e->getSubDirectory(FileHandlerBase::UserPresets); + if (userPresetsDir.isDirectory() && !allRoots.contains(userPresetsDir)) + allRoots.add(userPresetsDir); + } + } + + for (auto& rootDir : allRoots) + { + auto db = loadDatabaseForRoot(rootDir); + + Array presets; + rootDir.findChildFiles(presets, File::findFiles, true); + DataBaseHelpers::cleanFileList(const_cast(getMainController()), presets); + + for (auto& preset : presets) + { + if (DataBaseHelpers::isFavorite(db, preset)) + cachedFavorites.add(preset); + } + } + + favoritesCacheDirty = false; +} + +bool PresetBrowser::isFavoriteInAnyDatabase(const File& presetFile) const +{ + if (favoritesCacheDirty) + rebuildFavoritesCache(); + + return cachedFavorites.contains(presetFile); +} + +void PresetBrowser::setFavoriteForFile(const File& presetFile, bool isFavorite) +{ + auto root = findRootForFile(presetFile); + + if (!root.isDirectory()) + return; + + var db = loadDatabaseForRoot(root); + DataBaseHelpers::setFavorite(db, presetFile, isFavorite); + saveDatabaseForRoot(db, root); + invalidateFavoritesCache(); +} + +Array PresetBrowser::getAllFavoritePresets() +{ + if (favoritesCacheDirty) + rebuildFavoritesCache(); + + if (currentlySelectedExpansion == nullptr) + return cachedFavorites; + + auto expansionRoot = currentlySelectedExpansion->getSubDirectory(FileHandlerBase::UserPresets); + Array filtered; + + for (auto& f : cachedFavorites) + if (f.isAChildOf(expansionRoot)) + filtered.add(f); + + return filtered; +} + void PresetBrowser::presetChanged(const File& newPreset) { // After we switched the expansions we need to make sure to run this logic so that it ca @@ -909,6 +1094,28 @@ void PresetBrowser::resized() if (tagList->isActive()) tagList->setBounds(listArea.removeFromTop(30)); + const int folderOffset = expansionColumn != nullptr ? 1 : 0; + const int numColumnsToShow = jlimit(1, 4, numColumns + folderOffset); + int columnWidths[4] = { 0, 0, 0, 0 }; + auto w = (double)getWidth(); + + if (columnWidthRatios.size() == numColumnsToShow) + { + for (int i = 0; i < numColumnsToShow; i++) + { + auto r = jlimit(0.0, 1.0, (double)columnWidthRatios[i]); + columnWidths[i] = roundToInt(w * r); + } + } + else + { + // column amount mismatch, use equal spacing... + const int columnWidth = roundToInt(w / (double)numColumnsToShow); + + for (int i = 0; i < numColumnsToShow; i++) + columnWidths[i] = columnWidth; + } + if (showOnlyPresets) { if (expansionColumn != nullptr) @@ -918,28 +1125,6 @@ void PresetBrowser::resized() } else { - const int folderOffset = expansionColumn != nullptr ? 1 : 0; - const int numColumnsToShow = jlimit(1, 4, numColumns + folderOffset); - int columnWidths[4] = { 0, 0, 0, 0 }; - auto w = (double)getWidth(); - - if (columnWidthRatios.size() == numColumnsToShow) - { - for (int i = 0; i < numColumnsToShow; i++) - { - auto r = jlimit(0.0, 1.0, (double)columnWidthRatios[i]); - columnWidths[i] = roundToInt(w * r); - } - } - else - { - // column amount mismatch, use equal spacing... - const int columnWidth = roundToInt(w / (double)numColumnsToShow); - - for (int i = 0; i < numColumnsToShow; i++) - columnWidths[i] = columnWidth; - } - if(expansionColumn != nullptr) expansionColumn->setBounds(listArea.removeFromLeft(columnWidths[0]).reduced(2, 2)); @@ -1015,7 +1200,7 @@ void PresetBrowser::labelTextChanged(Label* l) { showOnlyPresets = !currentTagSelection.isEmpty() || l->getText().isNotEmpty() || favoriteButton->getToggleState(); - if (showOnlyPresets) + if (l->getText().isNotEmpty()) currentWildcard = "*" + l->getText() + "*"; else currentWildcard = "*"; @@ -1038,6 +1223,9 @@ void PresetBrowser::updateFavoriteButton() if (presetColumn == nullptr) return; + if (on) + invalidateFavoritesCache(); + presetColumn->setShowFavoritesOnly(on); resized(); @@ -1093,6 +1281,11 @@ void PresetBrowser::setShowFullPathFavorites(bool shouldShowFullPathFavorites) fullPathFavorites = shouldShowFullPathFavorites; } +void PresetBrowser::setShowFullPathSearch(bool shouldShowFullPathSearch) +{ + fullPathSearch = shouldShowFullPathSearch; +} + void PresetBrowser::setHighlightColourAndFont(Colour c, Colour bgColour, Font f) { auto& lf = getPresetBrowserLookAndFeel(); @@ -1313,6 +1506,17 @@ void PresetBrowser::setOptions(const Options& newOptions) setShowEditButtons(1, newOptions.showAddButton); setShowEditButtons(2, newOptions.showRenameButton); setShowEditButtons(3, newOptions.showDeleteButton); + + // Override expansion column buttons independently of the other columns. + // We hide individual buttons rather than disabling showButtonsAtBottom so that + // the 28px button area is still reserved, keeping the column height consistent + // with the bank/category/preset columns. + if (expansionColumn != nullptr && !newOptions.showExpansionEditButtons) + { + expansionColumn->setShowButtons(PresetBrowserColumn::AddButton, false); + expansionColumn->setShowButtons(PresetBrowserColumn::RenameButton, false); + expansionColumn->setShowButtons(PresetBrowserColumn::DeleteButton, false); + } setShowSearchBar(newOptions.showSearchBar); setButtonsInsideBorder(newOptions.buttonsInsideBorder); setEditButtonOffset(newOptions.editButtonOffset); @@ -1322,7 +1526,8 @@ void PresetBrowser::setOptions(const Options& newOptions) setShowFavorites(newOptions.showFavoriteIcons); setFavoriteIconOffset(newOptions.favoriteIconOffset); setShowFullPathFavorites(newOptions.fullPathFavorites); - + setShowFullPathSearch(newOptions.fullPathSearch); + if (expansionColumn != nullptr) expansionColumn->update(); @@ -1349,14 +1554,18 @@ void PresetBrowser::selectionChanged(int columnIndex, int /*rowIndex*/, const Fi currentBankFile = File(); currentCategoryFile = File(); currentlyLoadedPreset = 0; - + + // Flush before loadPresetDatabase overwrites the in-memory state. + if (rootFile.isDirectory()) + savePresetDatabase(rootFile); + if (file == File()) { if (FullInstrumentExpansion::isEnabled(getMainController())) rootFile = File(); else rootFile = defaultRoot; - + currentlySelectedExpansion = nullptr; } else @@ -1370,21 +1579,37 @@ void PresetBrowser::selectionChanged(int columnIndex, int /*rowIndex*/, const Fi } if(expansionColumn != nullptr) + { + if (file == File()) + expansionColumn->setSelectedFile(File()); expansionColumn->repaint(); + expansionColumn->updateButtonVisibility(false); + } bankColumn->setModel(new PresetBrowserColumn::ColumnListModel(this, 0, this), rootFile); bankColumn->setNewRootDirectory(rootFile); categoryColumn->setModel(new PresetBrowserColumn::ColumnListModel(this, 1, this), rootFile); categoryColumn->setNewRootDirectory(currentCategoryFile); - presetColumn->setNewRootDirectory(File()); - - auto pc = new PresetBrowserColumn::ColumnListModel(this, 2, this); - pc->setDisplayDirectories(false); - presetColumn->setModel(pc, rootFile); - + loadPresetDatabase(rootFile); - presetColumn->setDatabase(getDataBase()); rebuildAllPresets(); + + if (showOnlyPresets) + { + // Keep the existing model so the search wildcard is preserved; just + // refresh the list — getAllSearchRoots() now returns the new expansion. + presetColumn->setNewRootDirectory(rootFile); + } + else + { + presetColumn->setNewRootDirectory(File()); + + auto pc = new PresetBrowserColumn::ColumnListModel(this, 2, this); + pc->setDisplayDirectories(false); + presetColumn->setModel(pc, rootFile); + } + + presetColumn->setDatabase(getDataBase()); } if (columnIndex == 0) @@ -1462,6 +1687,7 @@ void PresetBrowser::renameEntry(int columnIndex, int rowIndex, const String& new if (newBank.isDirectory()) return; + updateDatabaseKeysForRename(currentBankFile, newBank); currentBankFile.moveFileTo(newBank); categoryColumn->setNewRootDirectory(File()); @@ -1481,6 +1707,7 @@ void PresetBrowser::renameEntry(int columnIndex, int rowIndex, const String& new if(newCategory.isDirectory()) return; + updateDatabaseKeysForRename(currentCategoryFile, newCategory); currentCategoryFile.moveFileTo(newCategory); categoryColumn->setNewRootDirectory(currentBankFile); @@ -1513,6 +1740,7 @@ void PresetBrowser::renameEntry(int columnIndex, int rowIndex, const String& new modalInputWindow->confirmReplacement(presetFile, newFile); else { + updateDatabaseKeyForFileRename(presetFile, newFile); presetFile.moveFileTo(newFile); presetColumn->setNewRootDirectory(current); rebuildAllPresets(); @@ -1904,4 +2132,78 @@ juce::Identifier PresetBrowser::DataBaseHelpers::getIdForFile(const File& preset return Identifier(); } +void PresetBrowser::DataBaseHelpers::renameEntriesInDatabase(const var& database, + const File& rootDir, + const File& oldDirectory, + const File& newDirectory) +{ + auto data = database.getDynamicObject(); + + if (data == nullptr) + return; + + auto makePrefix = [&](const File& dir) + { + auto s = dir.getRelativePathFrom(rootDir); + s = s.replaceCharacter('/', '_'); + s = s.replaceCharacter('\\', '_'); + s = s.replaceCharacter('\'', '_'); + s = s.removeCharacters(" \t!+&"); + return s; + }; + + auto oldPrefix = makePrefix(oldDirectory); + auto newPrefix = makePrefix(newDirectory); + + if (oldPrefix == newPrefix) + return; + + Array keysToRename; + Array valuesToMove; + + for (auto& prop : data->getProperties()) + { + if (prop.name.toString().startsWith(oldPrefix)) + { + keysToRename.add(prop.name); + valuesToMove.add(prop.value); + } + } + + for (int i = 0; i < keysToRename.size(); ++i) + { + auto oldKey = keysToRename[i].toString(); + auto newKey = newPrefix + oldKey.substring(oldPrefix.length()); + + data->removeProperty(keysToRename[i]); + + if (Identifier::isValidIdentifier(newKey)) + data->setProperty(Identifier(newKey), valuesToMove[i]); + } +} + +void PresetBrowser::DataBaseHelpers::renameFileEntryInDatabase(const var& database, + const File& oldFile, + const File& newFile) +{ + auto data = database.getDynamicObject(); + + if (data == nullptr) + return; + + auto oldId = getIdForFile(oldFile); + auto newId = getIdForFile(newFile); + + if (oldId.isNull() || newId.isNull() || oldId == newId) + return; + + auto value = data->getProperty(oldId); + + if (!value.isVoid()) + { + data->removeProperty(oldId); + data->setProperty(newId, value); + } +} + } // namespace hise diff --git a/hi_core/hi_components/plugin_components/PresetBrowser.h b/hi_core/hi_components/plugin_components/PresetBrowser.h index 3e10de59a5..e1b6bfe5e7 100644 --- a/hi_core/hi_components/plugin_components/PresetBrowser.h +++ b/hi_core/hi_components/plugin_components/PresetBrowser.h @@ -95,7 +95,9 @@ class PresetBrowser : public Component, bool showFolderButton = true; bool showFavoriteIcons = true; bool fullPathFavorites = false; + bool fullPathSearch = false; bool showExpansions = false; + bool showExpansionEditButtons = false; }; // ============================================================================================ @@ -186,6 +188,7 @@ class PresetBrowser : public Component, void updateFavoriteButton(); bool shouldShowFavoritesButton() { return showFavoritesButton; } bool shouldShowFullPathFavorites() { return fullPathFavorites; } + bool shouldShowFullPathSearch() { return fullPathSearch; } void lookAndFeelChanged() override; @@ -214,6 +217,9 @@ class PresetBrowser : public Component, static bool matchesAvailableExpansions(MainController* mc, const File& currentPreset); static bool isFavorite(const var& database, const File& presetFile); static Identifier getIdForFile(const File& presetFile); + static void renameEntriesInDatabase(const var& database, const File& rootDir, + const File& oldDirectory, const File& newDirectory); + static void renameFileEntryInDatabase(const var& database, const File& oldFile, const File& newFile); }; void setOptions(const Options& newOptions); @@ -233,6 +239,12 @@ class PresetBrowser : public Component, Point getMouseHoverInformation() const; + Array getAllSearchRoots() const; + Array getAllFavoritePresets(); + bool isFavoriteInAnyDatabase(const File& presetFile) const; + void setFavoriteForFile(const File& presetFile, bool isFavorite); + void invalidateFavoritesCache() { favoritesCacheDirty = true; } + Component* getColumn(int columnIndex) { switch(columnIndex) @@ -254,6 +266,7 @@ class PresetBrowser : public Component, void setShowFavorites(bool shouldShowFavorites); void setFavoriteIconOffset(int xOffset); void setShowFullPathFavorites(bool shouldShowFullPathFavorites); + void setShowFullPathSearch(bool shouldShowFullPathSearch); void setHighlightColourAndFont(Colour c, Colour bgColour, Font f); void setNumColumns(int numColumns); @@ -305,6 +318,7 @@ class PresetBrowser : public Component, bool showFavoritesButton = true; bool fullPathFavorites = false; + bool fullPathSearch = false; bool showOnlyPresets = false; String currentWildcard = "*"; StringArray currentTagSelection; @@ -313,6 +327,17 @@ class PresetBrowser : public Component, var presetDatabase; + // Lazily rebuilt cache of favourite files across all roots. + mutable Array cachedFavorites; + mutable bool favoritesCacheDirty = true; + + void rebuildFavoritesCache() const; + var loadDatabaseForRoot(const File& rootDir) const; + void saveDatabaseForRoot(const var& db, const File& rootDir); + File findRootForFile(const File& f) const; + void updateDatabaseKeysForRename(const File& oldDirectory, const File& newDirectory); + void updateDatabaseKeyForFileRename(const File& oldFile, const File& newFile); + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PresetBrowser); // ============================================================================================ diff --git a/hi_core/hi_components/plugin_components/PresetBrowserComponents.cpp b/hi_core/hi_components/plugin_components/PresetBrowserComponents.cpp index 2bf659b40c..953bf83ada 100644 --- a/hi_core/hi_components/plugin_components/PresetBrowserComponents.cpp +++ b/hi_core/hi_components/plugin_components/PresetBrowserComponents.cpp @@ -227,31 +227,25 @@ int PresetBrowserColumn::ColumnListModel::getNumRows() { if (wildcard.isEmpty() && currentlyActiveTags.isEmpty()) { - const File& rootToUse = showFavoritesOnly ? totalRoot : root; + if (showFavoritesOnly && index == 2) + { + entries = parent->getAllFavoritePresets(); + entries.sort(); + empty = entries.isEmpty(); + return entries.size(); + } - if (!rootToUse.isDirectory()) + if (!root.isDirectory()) { entries.clear(); return 0; } entries.clear(); - rootToUse.findChildFiles(entries, displayDirectories ? File::findDirectories : File::findFiles, allowRecursiveSearch || showFavoritesOnly); + root.findChildFiles(entries, displayDirectories ? File::findDirectories : File::findFiles, allowRecursiveSearch); PresetBrowser::DataBaseHelpers::cleanFileList(parent->getMainController(), entries); - if (showFavoritesOnly && index == 2) - { - for (int i = 0; i < entries.size(); i++) - { - if (!PresetBrowser::DataBaseHelpers::isFavorite(database, entries[i])) - { - entries.remove(i--); - continue; - } - } - } - entries.sort(); empty = entries.isEmpty(); return entries.size(); @@ -260,7 +254,14 @@ int PresetBrowserColumn::ColumnListModel::getNumRows() { jassert(index == 2); Array allFiles; - totalRoot.findChildFiles(allFiles, File::findFiles, true); + + auto searchRoots = parent->getAllSearchRoots(); + + if (searchRoots.isEmpty()) + totalRoot.findChildFiles(allFiles, File::findFiles, true); + else + for (auto& r : searchRoots) + r.findChildFiles(allFiles, File::findFiles, true); entries.clear(); for (int i = 0; i < allFiles.size(); i++) @@ -303,11 +304,8 @@ int PresetBrowserColumn::ColumnListModel::getNumRows() { for (int i = 0; i < entries.size(); i++) { - if (!PresetBrowser::DataBaseHelpers::isFavorite(database, entries[i])) - { + if (!parent->isFavoriteInAnyDatabase(entries[i])) entries.remove(i--); - continue; - } } } @@ -394,9 +392,28 @@ void PresetBrowserColumn::ColumnListModel::paintListBoxItem(int rowNumber, Graph auto column = parent->getColumn(index); jassert(dynamic_cast(column)->getModel() == this); - if (showFavoritesOnly && parent.getComponent()->shouldShowFullPathFavorites()) - itemName = entries[rowNumber].getRelativePathFrom(totalRoot); - + bool showFullPath = (showFavoritesOnly && parent.getComponent()->shouldShowFullPathFavorites()) + || (!wildcard.isEmpty() && parent.getComponent()->shouldShowFullPathSearch()); + + if (showFullPath) + { + const auto& f = entries[rowNumber]; + const auto searchRoots = parent->getAllSearchRoots(); + + itemName = f.getRelativePathFrom(totalRoot); + + for (int i = 0; i < searchRoots.size(); ++i) + { + if (f.isAChildOf(searchRoots[i])) + { + auto rel = f.getRelativePathFrom(searchRoots[i]); + itemName = (i > 0) ? searchRoots[i].getParentDirectory().getFileName() + File::getSeparatorString() + rel + : rel; + break; + } + } + } + getPresetBrowserLookAndFeel().drawListItem(g, *column, index, rowNumber, itemName, position, rowIsSelected, deleteOnClick, isMouseHover(rowNumber)); } } @@ -414,15 +431,34 @@ const juce::Array& PresetBrowse Component* PresetBrowserColumn::ColumnListModel::refreshComponentForRow(int rowNumber, bool /*isRowSelected*/, Component* existingComponentToUpdate) { - if (existingComponentToUpdate != nullptr) - delete existingComponentToUpdate; - if (index == 2 && parent.getComponent()->shouldShowFavoritesButton()) { + // Reuse existing overlays to avoid flicker, but only if the parent + // reference is still valid (setModel() may have replaced the model, + // leaving stale overlays in JUCE's spare-component cache). + if (auto* existing = dynamic_cast(existingComponentToUpdate)) + { + if (&existing->parent == this) + { + existing->refreshIndex(rowNumber); + existing->refreshShape(); + return existing; + } + + delete existing; + existingComponentToUpdate = nullptr; + } + + if (existingComponentToUpdate != nullptr) + delete existingComponentToUpdate; + return new FavoriteOverlay(*this, rowNumber); } - else - return nullptr; + + if (existingComponentToUpdate != nullptr) + delete existingComponentToUpdate; + + return nullptr; } void PresetBrowserColumn::ColumnListModel::sendRowChangeMessage(int row) @@ -460,7 +496,7 @@ void PresetBrowserColumn::ColumnListModel::FavoriteOverlay::refreshShape() { auto f = parent.getFileForIndex(index); - const bool on = PresetBrowser::DataBaseHelpers::isFavorite(parent.database, f); + const bool on = parent.isFavoriteInAnyDatabase(f); auto path = parent.getPresetBrowserLookAndFeel().createPresetBrowserIcons(on ? "favorite_on" : "favorite_off"); @@ -481,14 +517,22 @@ void PresetBrowserColumn::ColumnListModel::FavoriteOverlay::refreshShape() } +bool PresetBrowserColumn::ColumnListModel::isFavoriteInAnyDatabase(const File& f) const +{ + if (auto* pb = parent.getComponent()) + return pb->isFavoriteInAnyDatabase(f); + return false; +} + + void PresetBrowserColumn::ColumnListModel::FavoriteOverlay::buttonClicked(Button*) { const bool newValue = !b->getToggleState(); auto f = parent.getFileForIndex(index); - PresetBrowser::DataBaseHelpers::setFavorite(parent.database, f, newValue); - + if (auto* pb = findParentComponentOfClass()) + pb->setFavoriteForFile(f, newValue); refreshShape(); @@ -652,11 +696,161 @@ void PresetBrowserColumn::buttonClicked(Button* b) } else if (b == addButton) { + if (index == -1) + { + // Expansion column: install expansion from .hr1 package file + FileChooser fc("Select Expansion Package", File(), "*.hr1", true); + + if (fc.browseForFileToOpen()) + { + auto hr1File = fc.getResult(); + auto& expHandler = mc->getExpansionHandler(); + + auto targetFolder = expHandler.getExpansionTargetFolder(hr1File); + + if (targetFolder == File()) + { + PresetHandler::showMessageWindow("Invalid Package", "Could not read the expansion package metadata.", PresetHandler::IconType::Error); + return; + } + + auto existingExpansion = expHandler.getExpansionFromRootFile(targetFolder); + + File sampleDirectory; + + if (existingExpansion != nullptr) + { + // Expansion already installed: reuse existing sample location + sampleDirectory = existingExpansion->getSubDirectory(FileHandlerBase::Samples); + } + else + { + // New expansion: prompt for sample install location + FileChooser sampleFc("Select Sample Install Location"); + + if (sampleFc.browseForDirectory()) + sampleDirectory = sampleFc.getResult(); + else + return; + + // Put samples in a subfolder named after the expansion unless the + // chosen directory already matches the expansion name (case-insensitive, + // treating spaces, underscores, and dashes as equivalent). + auto normalize = [](String s) { + return s.toLowerCase().replaceCharacters("-_", " "); + }; + + auto expansionName = targetFolder.getFileName(); + + if (normalize(sampleDirectory.getFileName()) != normalize(expansionName)) + sampleDirectory = sampleDirectory.getChildFile(expansionName); + + sampleDirectory.createDirectory(); + } + + expHandler.installFromResourceFile(hr1File, sampleDirectory); + } + return; + } + parent->openModalAction(PresetBrowser::ModalWindow::Action::Add, index == 2 ? "New Preset" : "New Directory", File(), index, -1); } #if !OLD_PRESET_BROWSER else if (b == renameButton) { + if (index == -1) + { + // Expansion column: relocate samples for the selected expansion + if (auto ecm = dynamic_cast(listModel.get())) + { + int selectedIdx = ecm->lastIndex; + + if (selectedIdx >= 0) + { + auto rootFolder = ecm->getFileForIndex(selectedIdx); + + if (auto expansion = mc->getExpansionHandler().getExpansionFromRootFile(rootFolder)) + { + auto expName = expansion->getProperty(ExpansionIds::Name); + FileChooser fc("Select new sample location for '" + expName + "'"); + + if (fc.browseForDirectory()) + { + auto selectedDir = fc.getResult(); + + // Validate that the selected folder contains the expected monolith + // files for all sample maps in this expansion. + // + // Ensure the pool is populated before checking. + // FullInstrumentExpansion uses lazy loading, so the pool may be + // empty if the expansion hasn't been activated yet. + expansion->loadSampleMapsIfEmpty(); + + StringArray missingSampleMaps; + + auto checkMonolithRef = [&](const ValueTree& v) + { + MonolithFileReference mref(v); + + if (!mref.isUsingMonolith()) + return; + + mref.setFileNotFoundBehaviour(MonolithFileReference::FileNotFoundBehaviour::DoNothing); + mref.addSampleDirectory(selectedDir); + + if (!mref.getFile(false).existsAsFile()) + missingSampleMaps.add(mref.referenceString); + }; + + auto& smPool = expansion->pool->getSampleMapPool(); + auto sampleMapRefs = smPool.getListOfAllReferences(true); + + if (sampleMapRefs.isEmpty()) + { + // File-based expansion fallback: read XMLs directly from disk. + Array sampleMapFiles; + expansion->getSubDirectory(FileHandlerBase::SampleMaps) + .findChildFiles(sampleMapFiles, File::findFiles, true, "*.xml"); + + for (auto& smFile : sampleMapFiles) + { + if (auto xml = XmlDocument::parse(smFile)) + checkMonolithRef(ValueTree::fromXml(*xml)); + } + } + else + { + // Pool-based (HXI): all sample maps are already loaded. + for (auto& ref : sampleMapRefs) + { + auto entry = smPool.loadFromReference(ref, PoolHelpers::LoadingType::DontCreateNewEntry); + + if (entry != nullptr) + checkMonolithRef(entry->data); + } + } + + if (!missingSampleMaps.isEmpty()) + { + PresetHandler::showMessageWindow("Missing Sample Files", + "The selected folder is missing sample files for:\n" + missingSampleMaps.joinIntoString("\n"), + PresetHandler::IconType::Warning); + return; + } + + expansion->createLinkFile(FileHandlerBase::Samples, selectedDir); + expansion->checkSubDirectories(); + + PresetHandler::showMessageWindow("Sample Folder Relocated", + "The sample folder for '" + expName + "' has been successfully relocated to:\n" + selectedDir.getFullPathName(), + PresetHandler::IconType::Info); + } + } + } + } + return; + } + int selectedIndex = listbox->getSelectedRow(0); if (selectedIndex >= 0) @@ -668,6 +862,102 @@ void PresetBrowserColumn::buttonClicked(Button* b) } else if (b == deleteButton) { + if (index == -1) + { + // Expansion column: uninstall the selected expansion and its samples + if (auto ecm = dynamic_cast(listModel.get())) + { + int selectedIdx = ecm->lastIndex; + + if (selectedIdx >= 0) + { + auto rootFolder = ecm->getFileForIndex(selectedIdx); + + if (auto expansion = mc->getExpansionHandler().getExpansionFromRootFile(rootFolder)) + { + auto expName = expansion->getProperty(ExpansionIds::Name); + + if (!PresetHandler::showYesNoWindow("Uninstall Expansion", + "Are you sure you want to uninstall '" + expName + "' and its samples?")) + return; + + bool removePresets = PresetHandler::showYesNoWindow("Remove User Presets", + "Do you want to remove your custom presets for '" + expName + "'?\n\nSelect 'No' to keep your presets."); + + // getSubDirectory() returns the already-resolved path (follows link files). + // The expansion is still loaded here because unloadExpansion() is called last. + auto samplesDir = expansion->getSubDirectory(FileHandlerBase::Samples); + bool samplesAreExternal = !samplesDir.isAChildOf(rootFolder); + + // Don't touch the samples folder if it's shared (parent has project_info.xml). + bool safeToDeleteSamples = !samplesDir.getParentDirectory() + .getChildFile("project_info.xml") + .existsAsFile(); + + // Deletes all .ch* monolith files from a directory, then removes the + // directory itself if empty. Uses .ch* prefix (HISE's monolith format). + auto deleteMonolithFiles = [](const File& dir) + { + if (!dir.isDirectory()) + return; + + Array files; + dir.findChildFiles(files, File::findFiles, false); + + for (auto& f : files) + if (f.getFileExtension().startsWith(".ch")) + f.deleteFile(); + + if (dir.getNumberOfChildFiles(File::findFilesAndDirectories) == 0) + dir.deleteFile(); + }; + + if (safeToDeleteSamples && samplesAreExternal) + deleteMonolithFiles(samplesDir); + + if (removePresets) + { + rootFolder.deleteRecursively(); + } + else + { + Array children; + rootFolder.findChildFiles(children, File::findFilesAndDirectories, false); + + for (auto& child : children) + { + if (child.getFileName() == "UserPresets") + continue; + + if (!samplesAreExternal && child.getFileName() == "Samples" && child.isDirectory()) + { + if (safeToDeleteSamples) + deleteMonolithFiles(child); + continue; + } + + if (child.isDirectory()) + child.deleteRecursively(); + else + child.deleteFile(); + } + } + + mc->getExpansionHandler().unloadExpansion(expansion); + + ecm->setLastIndex(-1); + listbox->deselectAllRows(); + listbox->updateContent(); + listbox->repaint(); + updateButtonVisibility(false); + + parent->selectionChanged(-1, -1, File(), false); + } + } + } + return; + } + int selectedIndex = listbox->getSelectedRow(0); if (selectedIndex >= 0) @@ -731,7 +1021,7 @@ void PresetBrowserColumn::paint(Graphics& g) StringArray columnNames = { "Expansion", "Nothing", "Bank", "Column" }; - if (currentRoot == File() && listModel->wildcard.isEmpty() && listModel->currentlyActiveTags.isEmpty()) + if (currentRoot == File() && listModel->wildcard.isEmpty() && listModel->currentlyActiveTags.isEmpty() && !listModel->getShowFavoritesOnly()) emptyText = "Select a " + columnNames[jlimit(0, 3, index+1)]; else if (listModel->isEmpty()) emptyText = isResultBar ? "No results" : "Add a " + name; @@ -784,7 +1074,10 @@ void PresetBrowserColumn::updateButtonVisibility(bool isReadOnly) { editButton->setVisible(false); - const bool buttonsVisible = showButtonsAtBottom && !isResultBar && currentRoot.isDirectory() && !isReadOnly; + // The expansion column (index == -1) doesn't use setNewRootDirectory so currentRoot + // is never set; bypass the directory check for it + const bool rootOk = (index == -1) || currentRoot.isDirectory(); + const bool buttonsVisible = showButtonsAtBottom && !isResultBar && rootOk && !isReadOnly; const bool fileIsSelected = listbox->getNumSelectedRows() > 0; addButton->setVisible(buttonsVisible && shouldShowAddButton); diff --git a/hi_core/hi_components/plugin_components/PresetBrowserComponents.h b/hi_core/hi_components/plugin_components/PresetBrowserComponents.h index d40749cf8f..e81078056b 100644 --- a/hi_core/hi_components/plugin_components/PresetBrowserComponents.h +++ b/hi_core/hi_components/plugin_components/PresetBrowserComponents.h @@ -310,11 +310,15 @@ class PresetBrowserColumn : public Component, showFavoritesOnly = shouldShowFavoritesOnly; } + bool getShowFavoritesOnly() const { return showFavoritesOnly; } + File getFileForIndex(int fileIndex) const { return entries[fileIndex]; }; + bool isFavoriteInAnyDatabase(const File& f) const; + int getIndexForFile(const File& f) const { return entries.indexOf(f); @@ -417,20 +421,20 @@ class PresetBrowserColumn : public Component, return favoriteIconOffset; } + enum ButtonIndexes + { + All = 0, + AddButton, + RenameButton, + DeleteButton + }; + void setShowButtons(int buttonId, bool shouldBeShown) { - enum ButtonIndexes - { - All = 0, - AddButton, - RenameButton, - DeleteButton - }; - switch (buttonId) { - case All: showButtonsAtBottom = shouldBeShown; break; - case AddButton: shouldShowAddButton = shouldBeShown; break; + case All: showButtonsAtBottom = shouldBeShown; break; + case AddButton: shouldShowAddButton = shouldBeShown; break; case RenameButton: shouldShowRenameButton = shouldBeShown; break; case DeleteButton: shouldShowDeleteButton = shouldBeShown; break; } diff --git a/hi_core/hi_core/ExpansionHandler.cpp b/hi_core/hi_core/ExpansionHandler.cpp index 102105c5c3..441b279183 100644 --- a/hi_core/hi_core/ExpansionHandler.cpp +++ b/hi_core/hi_core/ExpansionHandler.cpp @@ -482,8 +482,19 @@ bool ExpansionHandler::installFromResourceFile(const File& resourceFile, const F auto expToSend = getExpansionFromRootFile(expRoot); if(expToSend != nullptr) + { expToSend->initialise(); + // Force-extract user presets after a fresh install so the preset + // browser is populated immediately without requiring a manual rebuild. + if (auto se = dynamic_cast(expToSend)) + { + ValueTree v; + if (se->loadValueTree(v).wasOk()) + se->extractUserPresetsIfEmpty(v, true); + } + } + for (auto l : listeners) { if (l.get() != nullptr) @@ -850,6 +861,12 @@ Result Expansion::initialise() return Result::ok(); } +void Expansion::loadSampleMapsIfEmpty() +{ + if (pool->getSampleMapPool().getNumLoadedFiles() == 0) + pool->getSampleMapPool().loadAllFilesFromProjectFolder(); +} + template void Expansion::Helpers::initCachedValue(ValueTree v, const T& cachedValue) { diff --git a/hi_core/hi_core/ExpansionHandler.h b/hi_core/hi_core/ExpansionHandler.h index 7239a62be6..003c89dffa 100644 --- a/hi_core/hi_core/ExpansionHandler.h +++ b/hi_core/hi_core/ExpansionHandler.h @@ -99,6 +99,10 @@ class Expansion: public FileHandlerBase */ virtual Result initialise();; + /** Ensures the sample map pool is populated, loading from disk if needed. + * Call this before iterating the pool for validation purposes. */ + virtual void loadSampleMapsIfEmpty(); + struct Helpers { static ValueTree loadValueTreeForFileBasedExpansion(const File& root);; diff --git a/hi_core/hi_core/PresetHandler.cpp b/hi_core/hi_core/PresetHandler.cpp index 096864c534..d934b59c2c 100644 --- a/hi_core/hi_core/PresetHandler.cpp +++ b/hi_core/hi_core/PresetHandler.cpp @@ -2738,17 +2738,12 @@ void FileHandlerBase::createLinkFileInFolder(const File& source, const File& tar { if(linkFile.loadFileAsString() == target.getFullPathName()) return; - + if (!target.isDirectory()) { linkFile.deleteFile(); return; } - - if (!PresetHandler::showYesNoWindowIfMessageThread("Already there", "Link redirect file exists. Do you want to replace it?", true)) - { - return; - } } if(!target.isDirectory()) diff --git a/hi_scripting/scripting/api/ScriptExpansion.cpp b/hi_scripting/scripting/api/ScriptExpansion.cpp index e3cb196766..b25e0b9369 100644 --- a/hi_scripting/scripting/api/ScriptExpansion.cpp +++ b/hi_scripting/scripting/api/ScriptExpansion.cpp @@ -2554,6 +2554,37 @@ juce::Result FullInstrumentExpansion::initialise() } +void FullInstrumentExpansion::loadSampleMapsIfEmpty() +{ + // For non-lazy-loaded expansion types just use the base class (reads from disk). + if (getExpansionType() != Expansion::Intermediate) + { + Expansion::loadSampleMapsIfEmpty(); + return; + } + + // FullInstrumentExpansion uses lazy loading: the pool is empty until the + // expansion is actually activated. Populate just the sample maps so that + // the redirect-sample validation in the preset browser can check them. + if (pool->getSampleMapPool().getNumLoadedFiles() > 0) + return; + + // A valid Blowfish key is required to decrypt the embedded pool data. + ScopedPointer bf = createBlowfish(); + + if (bf == nullptr) + return; + + auto allData = getValueTreeFromFile(getExpansionType()); + + if (!allData.isValid()) + return; + + setCompressorForPool(FileHandlerBase::SampleMaps, true); + restorePool(allData, FileHandlerBase::SampleMaps); + pool->getSampleMapPool().loadAllFilesFromDataProvider(); +} + juce::ValueTree FullInstrumentExpansion::getValueTreeFromFile(Expansion::ExpansionType type) { auto hxiFile = Helpers::getExpansionInfoFile(getRootFolder(), type); diff --git a/hi_scripting/scripting/api/ScriptExpansion.h b/hi_scripting/scripting/api/ScriptExpansion.h index 5251ce6ef2..ea82aca2a1 100644 --- a/hi_scripting/scripting/api/ScriptExpansion.h +++ b/hi_scripting/scripting/api/ScriptExpansion.h @@ -424,6 +424,8 @@ class FullInstrumentExpansion : public ScriptEncryptedExpansion, Result initialise() override; + void loadSampleMapsIfEmpty() override; + Result encodeExpansion() override; ValueTree getEmbeddedNetwork(const String& id) override;