diff --git a/.gitignore b/.gitignore index b41816c2..9722f889 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ libtorch/ testproject scratch _downloads +artifacts/ packaging/dmg packaging/*.dmg diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..a31c1439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,8 @@ target_sources(${PROJECT_NAME} src/Main.cpp src/Application.cpp src/MainComponent.cpp + src/HomeTab.h + src/ModelTabContainer.h src/ModelTab.h src/Model.h @@ -111,6 +113,7 @@ target_sources(${PROJECT_NAME} src/utils/Logging.h src/utils/Settings.h src/utils/Interface.h + src/utils/ModelRegistry.h src/utils/Controls.h src/utils/Labels.h src/utils/Clients.h diff --git a/src/HomeTab.h b/src/HomeTab.h new file mode 100644 index 00000000..f70f921b --- /dev/null +++ b/src/HomeTab.h @@ -0,0 +1,617 @@ +/** + * @file HomeTab.h + * @brief Home tab for model discovery and loading. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "utils/Interface.h" +#include "utils/ModelRegistry.h" +#include "widgets/ModelSelectionWidget.h" + +using namespace juce; + +class TagLabel : public Component +{ +public: + TagLabel(const String& text) : tagText(text) + { + setSize(getPreferredWidth(), getPreferredHeight()); + } + + int getPreferredWidth() const { return font.getStringWidth(tagText) + horizontalPadding * 2; } + int getPreferredHeight() const { return roundToInt(font.getHeight()) + verticalPadding * 2; } + + void paint(Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(0.5f); + g.setColour(Colour(0xff183238)); + g.fillRoundedRectangle(bounds, 4.0f); + g.setColour(Colour(0xff2dd4bf).withAlpha(0.42f)); + g.drawRoundedRectangle(bounds, 4.0f, 1.0f); + + g.setColour(Colour(0xff9eeadf)); + g.setFont(font); + g.drawText(tagText, getLocalBounds(), Justification::centred, true); + } + +private: + String tagText; + Font font { 10.0f, Font::bold }; + static constexpr int horizontalPadding = 7; + static constexpr int verticalPadding = 4; +}; + +class CategoryChip : public Button +{ +public: + CategoryChip(const String& name, bool selected) + : Button(name), isSelected(selected) + { + } + + void setSelected(bool selected) + { + if (isSelected != selected) + { + isSelected = selected; + repaint(); + } + } + + bool getSelected() const { return isSelected; } + + int getPreferredWidth() const { return font.getStringWidth(getName()) + horizontalPadding * 2; } + int getPreferredHeight() const { return roundToInt(font.getHeight()) + verticalPadding * 2; } + + void paintButton(Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + + Colour bg; + Colour textColour; + + if (isSelected) + { + bg = Colour(0xff0f766e); + textColour = Colours::white; + } + else if (shouldDrawButtonAsHighlighted || shouldDrawButtonAsDown) + { + bg = Colour(0xff263a3d); + textColour = Colours::white; + } + else + { + bg = Colour(0xff1e1e24); + textColour = Colours::lightgrey; + } + + g.setColour(bg); + g.fillRoundedRectangle(bounds, 5.0f); + + g.setColour(isSelected ? Colour(0xff5eead4) : Colours::white.withAlpha(0.1f)); + g.drawRoundedRectangle(bounds, 5.0f, 1.0f); + + g.setColour(textColour); + g.setFont(font); + g.drawText(getName(), getLocalBounds().reduced(horizontalPadding, 0), Justification::centred, true); + } + +private: + bool isSelected = false; + Font font { 13.0f, Font::bold }; + static constexpr int horizontalPadding = 12; + static constexpr int verticalPadding = 7; +}; + +class CategoryFilterBar : public Component +{ +public: + CategoryFilterBar(std::function onCategorySelectedCallback) + : onCategorySelected(std::move(onCategorySelectedCallback)) + { + categories = { + "All", + "Generation", + "Performance Rendering and Synthesis", + "Effects", + "Enhancement", + "Production", + "Source Separation", + "Analysis", + "Custom" + }; + + for (int i = 0; i < categories.size(); ++i) + { + auto chip = std::make_unique(categories[i], i == 0); + chip->onClick = [this, category = categories[i]] + { + selectCategory(category); + }; + addAndMakeVisible(*chip); + chips.push_back(std::move(chip)); + } + } + + void selectCategory(const String& category) + { + for (auto& chip : chips) + { + chip->setSelected(chip->getName() == category); + } + + if (onCategorySelected) + onCategorySelected(category); + } + + void resized() override + { + auto area = getLocalBounds(); + int x = 0; + int y = 0; + int spacingX = 6; + int spacingY = 6; + int rowHeight = 0; + + for (auto& chip : chips) + { + int chipWidth = chip->getPreferredWidth(); + int chipHeight = chip->getPreferredHeight(); + + if (x + chipWidth > area.getWidth() && x > 0) + { + x = 0; + y += rowHeight + spacingY; + rowHeight = 0; + } + + chip->setBounds(x, y, chipWidth, chipHeight); + x += chipWidth + spacingX; + rowHeight = jmax(rowHeight, chipHeight); + } + + int newHeight = y + jmax(rowHeight, 1); + if (newHeight != preferredHeight) + { + preferredHeight = newHeight; + MessageManager::callAsync([this]() + { + if (auto* parent = getParentComponent()) + parent->resized(); + }); + } + } + + int getPreferredHeight() const { return preferredHeight; } + +private: + std::vector categories; + std::vector> chips; + std::function onCategorySelected; + int preferredHeight = 28; +}; + +class CategoryHeader : public Component +{ +public: + CategoryHeader(const String& name) : categoryName(name) {} + + void paint(Graphics& g) override + { + auto bounds = getLocalBounds().toFloat(); + + g.setColour(Colours::white); + g.setFont(Font(16.0f, Font::bold)); + g.drawText(categoryName, getLocalBounds().reduced(4, 0), Justification::centredLeft, true); + + auto textWidth = Font(16.0f, Font::bold).getStringWidth(categoryName); + g.setColour(Colour(0xff2dd4bf).withAlpha(0.6f)); + g.fillRect(textWidth + 12.0f, bounds.getCentreY() - 1.0f, bounds.getWidth() - textWidth - 16.0f, 2.0f); + } + + static constexpr int preferredHeight = 32; + +private: + String categoryName; +}; + +class ModelRegistryCard : public Component +{ +public: + ModelRegistryCard(ModelRegistry::Entry registryEntry, + std::function loadCallback) + : entry(std::move(registryEntry)), onLoad(std::move(loadCallback)) + { + nameLabel.setText(entry.displayName, dontSendNotification); + nameLabel.setJustificationType(Justification::centredLeft); + nameLabel.setFont(Font(17.0f, Font::bold)); + addAndMakeVisible(nameLabel); + + providerLabel.setText(entry.provider, dontSendNotification); + providerLabel.setJustificationType(Justification::centredLeft); + providerLabel.setColour(Label::textColourId, Colours::lightgrey); + addAndMakeVisible(providerLabel); + + summaryLabel.setText(entry.summary, dontSendNotification); + summaryLabel.setJustificationType(Justification::centredLeft); + summaryLabel.setColour(Label::textColourId, Colours::whitesmoke); + addAndMakeVisible(summaryLabel); + + pathLabel.setText(entry.path, dontSendNotification); + pathLabel.setJustificationType(Justification::centredLeft); + pathLabel.setColour(Label::textColourId, Colours::grey); + addAndMakeVisible(pathLabel); + + loadButton.setButtonText("Load"); + loadButton.onClick = [this] + { + if (onLoad) + onLoad(entry); + }; + addAndMakeVisible(loadButton); + + for (const auto& tag : entry.tags) + { + auto label = std::make_unique(tag); + addAndMakeVisible(*label); + tagLabels.push_back(std::move(label)); + } + } + + void paint(Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + g.setColour(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::widgetBackground) + .brighter(0.06f)); + g.fillRoundedRectangle(bounds, 6.0f); + + g.setColour(Colours::white.withAlpha(0.12f)); + g.drawRoundedRectangle(bounds, 6.0f, 1.0f); + } + + void resized() override + { + auto area = getLocalBounds().reduced(12, 10); + auto buttonArea = area.removeFromRight(92); + loadButton.setBounds(buttonArea.withSizeKeepingCentre(80, 30)); + + auto topRow = area.removeFromTop(18); + providerLabel.setBounds(topRow.removeFromLeft(150)); + + for (auto& tagLabel : tagLabels) + { + tagLabel->setBounds(topRow.removeFromRight(tagLabel->getPreferredWidth() + 4) + .withSizeKeepingCentre(tagLabel->getPreferredWidth(), + tagLabel->getPreferredHeight())); + } + + nameLabel.setBounds(area.removeFromTop(24)); + summaryLabel.setBounds(area.removeFromTop(24)); + pathLabel.setBounds(area.removeFromTop(18)); + } + + static constexpr int preferredHeight = 104; + +private: + ModelRegistry::Entry entry; + std::function onLoad; + + Label nameLabel; + Label providerLabel; + Label summaryLabel; + Label pathLabel; + TextButton loadButton; + std::vector> tagLabels; +}; + +class ModelRegistryList : public Component +{ +public: + struct Section + { + String category; + std::vector entries; + }; + + void setSections(std::vector
newSections, + std::function loadCallback) + { + items.clear(); + removeAllChildren(); + + for (auto& sec : newSections) + { + if (sec.entries.empty()) + continue; + + auto header = std::make_unique(sec.category); + addAndMakeVisible(*header); + items.push_back(std::move(header)); + + for (auto& entry : sec.entries) + { + auto card = std::make_unique(std::move(entry), loadCallback); + addAndMakeVisible(*card); + items.push_back(std::move(card)); + } + } + + resized(); + repaint(); + } + + void resized() override + { + auto area = getLocalBounds(); + + for (auto& item : items) + { + if (dynamic_cast(item.get())) + item->setBounds(area.removeFromTop(CategoryHeader::preferredHeight)); + else if (dynamic_cast(item.get())) + item->setBounds(area.removeFromTop(ModelRegistryCard::preferredHeight).reduced(0, 4)); + } + } + + int getRequiredHeight() const + { + int height = 0; + for (const auto& item : items) + { + if (dynamic_cast(item.get())) + height += CategoryHeader::preferredHeight; + else if (dynamic_cast(item.get())) + height += ModelRegistryCard::preferredHeight; + } + return height; + } + +private: + std::vector> items; +}; + +class HomeTab : public Component, + private ChangeListener +{ +public: + HomeTab() + { + sharedChoices->addChangeListener(this); + + titleLabel.setText("Models", dontSendNotification); + titleLabel.setJustificationType(Justification::centredLeft); + titleLabel.setFont(Font(24.0f, Font::bold)); + + subtitleLabel.setText("Search HARP-compatible models and open one in a new tab.", + dontSendNotification); + subtitleLabel.setJustificationType(Justification::centredLeft); + + searchEditor.setTextToShowWhenEmpty("Search models...", Colours::grey); + searchEditor.setMultiLine(false); + searchEditor.setReturnKeyStartsNewLine(false); + searchEditor.onTextChange = [this] { rebuildModelList(); }; + + customPathButton.setButtonText("Custom Path"); + customPathButton.onClick = [this] { openCustomPathPopup(); }; + + viewport.setViewedComponent(&modelList, false); + viewport.setScrollBarsShown(true, false); + + addAndMakeVisible(titleLabel); + addAndMakeVisible(subtitleLabel); + addAndMakeVisible(searchEditor); + addAndMakeVisible(customPathButton); + addAndMakeVisible(categoryFilterBar); + addAndMakeVisible(viewport); + + rebuildModelList(); + } + + ~HomeTab() override + { + sharedChoices->removeChangeListener(this); + } + + void resized() override + { + auto area = getLocalBounds().reduced(16); + + titleLabel.setBounds(area.removeFromTop(34)); + subtitleLabel.setBounds(area.removeFromTop(26)); + + area.removeFromTop(8); + auto searchRow = area.removeFromTop(34); + customPathButton.setBounds(searchRow.removeFromRight(120).reduced(0, 1)); + searchRow.removeFromRight(8); + searchEditor.setBounds(searchRow); + + area.removeFromTop(10); + categoryFilterBar.setBounds(area.removeFromTop(categoryFilterBar.getPreferredHeight())); + + area.removeFromTop(10); + viewport.setBounds(area); + + updateListBounds(); + } + + void resetSelection() + { + searchEditor.setEnabled(true); + customPathButton.setEnabled(true); + categoryFilterBar.setEnabled(true); + viewport.setEnabled(true); + } + + Rectangle getModelSelectBounds() const + { + return searchEditor.getBounds().expanded(2, 2); + } + + std::function onModelLoadRequested; + +private: + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (source == static_cast(sharedChoices)) + rebuildModelList(); + } + + void requestModelLoad(const ModelRegistry::Entry& entry) + { + searchEditor.setEnabled(false); + customPathButton.setEnabled(false); + categoryFilterBar.setEnabled(false); + viewport.setEnabled(false); + + if (onModelLoadRequested) + onModelLoadRequested(entry.path, entry.displayName); + } + + void rebuildModelList() + { + std::vector entries; + const auto searchText = searchEditor.getText().trim().toLowerCase(); + + for (const auto& savedPath : sharedChoices->savedModelPaths) + { + const String path(savedPath); + + if (path.startsWithIgnoreCase("click here")) + continue; + + auto entry = ModelRegistry::getEntryForPath(path); + const auto searchableText = + (entry.displayName + " " + entry.summary + " " + entry.path + " " + entry.provider) + .toLowerCase(); + + if (searchText.isEmpty() || searchableText.contains(searchText)) + { + if (activeCategory == "All") + { + entries.push_back(std::move(entry)); + } + else if (activeCategory == "Custom") + { + if (entry.tags.empty()) + entries.push_back(std::move(entry)); + } + else + { + bool matchesCategory = false; + for (const auto& tag : entry.tags) + { + if (tag == activeCategory) + { + matchesCategory = true; + break; + } + } + if (matchesCategory) + entries.push_back(std::move(entry)); + } + } + } + + std::vector sections; + std::vector categoriesToShow; + + if (activeCategory == "All") + { + categoriesToShow = { + "Generation", + "Performance Rendering and Synthesis", + "Effects", + "Enhancement", + "Production", + "Source Separation", + "Analysis", + "Custom" + }; + } + else + { + categoriesToShow = { activeCategory }; + } + + for (const auto& cat : categoriesToShow) + { + ModelRegistryList::Section sec; + sec.category = cat; + + for (const auto& entry : entries) + { + if (cat == "Custom") + { + if (entry.tags.empty()) + sec.entries.push_back(entry); + } + else + { + for (const auto& tag : entry.tags) + { + if (tag == cat) + { + sec.entries.push_back(entry); + break; + } + } + } + } + + if (! sec.entries.empty()) + sections.push_back(std::move(sec)); + } + + modelList.setSections(std::move(sections), + [this](ModelRegistry::Entry entry) { requestModelLoad(entry); }); + updateListBounds(); + } + + void updateListBounds() + { + const auto width = jmax(0, viewport.getWidth() - viewport.getScrollBarThickness()); + modelList.setSize(width, jmax(viewport.getHeight(), modelList.getRequiredHeight())); + } + + void openCustomPathPopup() + { + std::function loadCallback = [this](String path) + { + auto entry = ModelRegistry::getEntryForPath(path); + requestModelLoad(entry); + }; + + auto* content = new CustomPathComponent(std::move(loadCallback), [] {}); + + DialogWindow::LaunchOptions options; + options.dialogTitle = "Enter Custom Path"; + options.dialogBackgroundColour = Colours::darkgrey; + options.content.setOwned(content); + + options.useNativeTitleBar = false; + options.resizable = false; + options.escapeKeyTriggersCloseButton = true; + options.componentToCentreAround = this; + + options.launchAsync(); + } + + Label titleLabel; + Label subtitleLabel; + TextEditor searchEditor; + TextButton customPathButton; + CategoryFilterBar categoryFilterBar { [this](String cat) { activeCategory = cat; rebuildModelList(); } }; + String activeCategory { "All" }; + Viewport viewport; + ModelRegistryList modelList; + + SharedResourcePointer sharedChoices; +}; diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index a31a32df..d0d2600c 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -10,9 +10,9 @@ MainComponent::MainComponent() initializeMenuBar(); - mainModelTab.addChangeListener(this); + modelTabs.addChangeListener(this); - addAndMakeVisible(mainModelTab); + addAndMakeVisible(modelTabs); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); @@ -31,7 +31,7 @@ MainComponent::MainComponent() MainComponent::~MainComponent() { deinitializeMenuBar(); - mainModelTab.removeChangeListener(this); + modelTabs.removeChangeListener(this); } void MainComponent::paint(Graphics& g) @@ -83,6 +83,16 @@ void MainComponent::paintOverChildren(Graphics& g) } } +ModelTab* MainComponent::getCurrentModelTab() const +{ + return modelTabs.getCurrentModelTab(); +} + +ModelTab* MainComponent::getFirstModelTab() const +{ + return modelTabs.getFirstModelTab(); +} + void MainComponent::resized() { Rectangle fullArea = getLocalBounds(); @@ -92,13 +102,20 @@ void MainComponent::resized() fullArea.removeFromTop(LookAndFeel::getDefaultLookAndFeel().getDefaultMenuBarHeight())); #endif + + FlexBox fullWindow; fullWindow.flexDirection = FlexBox::Direction::row; FlexBox mainPanel; mainPanel.flexDirection = FlexBox::Direction::column; - mainPanel.items.add(FlexItem(mainModelTab).withFlex(1.0)); + mainPanel.items.add(FlexItem(modelTabs).withFlex(1.0)); + + auto bounds = getLocalBounds(); + + // Give full area to tabs + modelTabs.setBounds(bounds); if (showStatusArea) { @@ -128,8 +145,13 @@ void MainComponent::resized() } } + + void MainComponent::updateWindowConstraints() { + auto* tab = getCurrentModelTab(); + if (!tab) return; + if (auto* window = findParentComponentOfClass()) { // Compute percentage of total window width given to main panel @@ -138,12 +160,13 @@ void MainComponent::updateWindowConstraints() // Determine minimum width needed to display controls plus padding const int requiredMainPanelWidth = jmax(minimumMainPanelWidth, - mainModelTab.getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); - // Determine current width of main panel - const int mainPanelWidth = jmax(requiredMainPanelWidth, mainModelTab.getWidth()); - // Determine minimum height needed to display all model contents plus status widget + tab->getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); + + const int mainPanelWidth = + jmax(requiredMainPanelWidth, tab->getWidth()); + const int requiredMainPanelHeight = - mainModelTab.getMinimumRequiredHeightForWidth(mainPanelWidth) + tab->getMinimumRequiredHeightForWidth(mainPanelWidth) + (showStatusArea ? statusAreaHeight : 0); // Determine effective minimum width of entire window @@ -407,61 +430,108 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::ensureTutorialModelLoaded() { - if (! mainModelTab.isModelLoaded()) - mainModelTab.loadDefaultModel(); + auto* tab = getCurrentModelTab(); + + if (tab == nullptr) + { + tab = modelTabs.createNewTab(); + modelTabs.setCurrentTabIndex(0); + + if (welcomeWindow != nullptr) + tab->addChangeListener(welcomeWindow.get()); + + if (tab != nullptr) + tab->loadDefaultModel(); + return; + } + + if (! tab->isModelLoaded()) + tab->loadDefaultModel(); } void MainComponent::resetTutorialAutoLoadedModel() { - if (! mainModelTab.isModelLoaded()) - return; - - if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) + if (auto* tab = getCurrentModelTab()) { - mainModelTab.resetState(); + if (tab->isModelLoaded() && tab->getLoadedPath() == TutorialConstants::fallbackModelPath) + tab->resetState(); } } Rectangle MainComponent::getModelSelectBounds() { - auto bounds = mainModelTab.getModelSelectBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* homeTab = dynamic_cast(modelTabs.getCurrentContentComponent())) + { + auto bounds = homeTab->getModelSelectBounds(); + return getLocalArea(homeTab, bounds); + } + + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getModelSelectBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getControlsBounds() { - auto bounds = mainModelTab.getControlsBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getControlsBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputTrackBounds() { - auto bounds = mainModelTab.getInputTrackBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputTrackBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputFolderBounds() { - auto bounds = mainModelTab.getInputFolderBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputFolderBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputPlayBounds() { - auto bounds = mainModelTab.getInputPlayBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputPlayBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getProcessButtonBounds() { - auto bounds = mainModelTab.getProcessButtonBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getProcessButtonBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getTracksBounds() { - auto bounds = mainModelTab.getTracksBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getTracksBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getClipboardBounds() @@ -607,7 +677,7 @@ void MainComponent::focusCallback() void MainComponent::changeListenerCallback(ChangeBroadcaster* source) { - if (source == &mainModelTab) + if (source == &modelTabs) { updateWindowConstraints(); } diff --git a/src/MainComponent.h b/src/MainComponent.h index b229d7a4..4d777918 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -9,6 +9,7 @@ #include #include "ModelTab.h" +#include "ModelTabContainer.h" #include "clients/Client.h" @@ -74,14 +75,16 @@ class MainComponent : public Component, /* Tutorial */ - ModelTab* getModelTab() { return &mainModelTab; } - void setTutorialActive(bool active); void setTutorialHighlight(Rectangle bounds); void setTutorialExtraHighlights(std::vector> bounds); void ensureTutorialModelLoaded(); void resetTutorialAutoLoadedModel(); + ModelTab* getCurrentModelTab() const; + ModelTab* getFirstModelTab() const; + + // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); Rectangle getControlsBounds(); @@ -126,7 +129,7 @@ class MainComponent : public Component, // Miscellaneous //void focusCallback(); - void changeListenerCallback(ChangeBroadcaster* source); + void changeListenerCallback(ChangeBroadcaster* source) override; /* Interface */ @@ -150,16 +153,17 @@ class MainComponent : public Component, bool showStatusArea; bool showMediaClipboard; - ModelTab mainModelTab; + + ModelTabContainer modelTabs; StatusAreaWidget statusAreaWidget; MediaClipboardWidget mediaClipboardWidget; - bool isTutorialActive = false; - Rectangle tutorialHighlightRect; - std::vector> tutorialExtraHighlights; - std::unique_ptr welcomeWindow; - - SharedResourcePointer sharedTokens; + bool isTutorialActive = false; + Rectangle tutorialHighlightRect; + std::vector> tutorialExtraHighlights; + std::unique_ptr welcomeWindow; + + SharedResourcePointer sharedTokens; SharedResourcePointer statusMessage; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) diff --git a/src/ModelTab.h b/src/ModelTab.h index 3c172ede..a8155bdf 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -6,6 +6,8 @@ #pragma once +#include + #include #include "Model.h" @@ -21,14 +23,13 @@ using namespace juce; -class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster +class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster { public: ModelTab() { modelSelectionWidget.addChangeListener(this); - addAndMakeVisible(modelSelectionWidget); addAndMakeVisible(modelInfoWidget); addAndMakeVisible(controlAreaWidget); @@ -58,6 +59,16 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas modelSelectionWidget.loadModelBypass(TutorialConstants::fallbackModelPath); } + void loadModelPath(const String& modelPath) + { + modelSelectionWidget.loadModelBypass(modelPath); + } + + void onNextModelLoadComplete(std::function callback) + { + initialLoadCallback = std::move(callback); + } + // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const { @@ -115,12 +126,7 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas /* Model Selection */ - tabArea.items.add(FlexItem(modelSelectionWidget) - .withHeight(modelSelectionRowHeight) - .withMinHeight(modelSelectionRowHeight) - .withMaxHeight(modelSelectionRowHeight) - .withFlex(0) - .withMargin(marginSize)); + modelSelectionWidget.setBounds(0, 0, 0, 0); /* Model Info */ @@ -198,7 +204,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas { int height = 0; - height += modelSelectionRowHeight + 2 * marginSize; height += modelInfoWidget.getPreferredHeightForWidth(width) + 2 * marginSize; if (controlAreaWidget.getNumControls() > 0) @@ -439,6 +444,8 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas // Re-enable processing immediately processCancelButton.setEnabled(true); + + notifyInitialLoadComplete(true); } else { @@ -450,6 +457,8 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas // Re-enable processing after closing error window processCancelButton.setEnabled(true); + + notifyInitialLoadComplete(false); }; openErrorPopup(error, onExit); @@ -458,6 +467,15 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas }); } + void notifyInitialLoadComplete(bool wasSuccessful) + { + auto callback = std::move(initialLoadCallback); + initialLoadCallback = nullptr; + + if (callback) + callback(this, wasSuccessful); + } + void processCallback() { std::map loadedInputFiles; @@ -609,4 +627,5 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool processingThreadPool { 10 }; std::atomic currentProcessID { 0 }; -}; \ No newline at end of file + std::function initialLoadCallback; +}; diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h new file mode 100644 index 00000000..7c9320b7 --- /dev/null +++ b/src/ModelTabContainer.h @@ -0,0 +1,260 @@ +/** + * @brief Adds tab container to HARP for MultiTabs + * @author JEYuhas + */ +#pragma once + +#include + +#include "HomeTab.h" +#include "Model.h" +#include "ModelTab.h" + +#include "widgets/ControlAreaWidget.h" +#include "widgets/ModelInfoWidget.h" +#include "widgets/ModelSelectionWidget.h" +#include "widgets/TrackAreaWidget.h" + +#include "utils/Errors.h" +#include "utils/Interface.h" +#include "utils/Logging.h" +#include "utils/ModelRegistry.h" +#include "utils/Tutorial.h" + +using namespace juce; + +class ModelTabsLookAndFeel : public LookAndFeel_V4 +{ +public: + void drawTabbedButtonBarBackground(TabbedButtonBar& bar, Graphics& g) override + { + g.fillAll(tabBarColour); + g.setColour(separatorColour); + g.fillRect(0, bar.getHeight() - 1, bar.getWidth(), 1); + } + + void drawTabAreaBehindFrontButton(TabbedButtonBar&, Graphics& g, int w, int h) override + { + g.setColour(separatorColour); + g.fillRect(0, h - 1, w, 1); + } + + void drawTabButton(TabBarButton& button, + Graphics& g, + bool isMouseOver, + bool isMouseDown) override + { + const auto isActive = button.isFrontTab(); + auto area = button.getActiveArea(); + + const auto fill = isActive + ? activeTabColour + : inactiveTabColour.brighter(isMouseOver || isMouseDown ? 0.08f : 0.0f); + + g.setColour(fill); + g.fillRect(area); + + if (button.getIndex() > 0) + { + g.setColour(separatorColour); + g.fillRect(area.getX(), area.getY() + 2, 1, area.getHeight() - 4); + } + + auto textArea = button.getTextArea().reduced(tabTextInset, 0); + + g.setColour(isActive ? activeTextColour + : inactiveTextColour); + + g.drawText(button.getButtonText(), + textArea, + Justification::centred, + true); + } + + int getTabButtonBestWidth(TabBarButton& button, int tabDepth) override + { + return button.getButtonText() == "Home" + ? homeTabWidth + : fixedTabWidth; + } + + void drawTabButtonText(TabBarButton&, + Graphics&, + bool /*isMouseOver*/, + bool /*isMouseDown*/) override + { + } + +private: + const Colour tabBarColour { Colour(0xff1f1f1f) }; + const Colour inactiveTabColour { Colour(0xff242424) }; + const Colour activeTabColour { Colour(0xff343434) }; + const Colour separatorColour { Colour(0xff4a4a4a) }; + const Colour activeTextColour { Colours::white }; + const Colour inactiveTextColour { Colour(0xffaeb0b4) }; + static constexpr int fixedTabWidth = 140; + static constexpr int homeTabWidth = 64; + static constexpr int tabTextInset = 10; +}; + +class ModelTabContainer : public TabbedComponent, + private ChangeListener, + public ChangeBroadcaster +{ +public: + ModelTabContainer() + : TabbedComponent(TabbedButtonBar::TabsAtTop) + { + getTabbedButtonBar().setLookAndFeel(&tabsLookAndFeel); + + setColour(TabbedComponent::backgroundColourId, tabBackgroundColour); + getTabbedButtonBar().setColour(TabbedButtonBar::tabTextColourId, Colours::white); + getTabbedButtonBar().setColour(TabbedButtonBar::frontTextColourId, Colours::white); + getTabbedButtonBar().setColour(TabbedButtonBar::tabOutlineColourId, tabBackgroundColour.darker(0.35f)); + getTabbedButtonBar().setColour(TabbedButtonBar::frontOutlineColourId, tabBackgroundColour.darker(0.35f)); + + createHomeTab(); + } + + ~ModelTabContainer() override + { + getTabbedButtonBar().setLookAndFeel(nullptr); + } + + ModelTab* createNewTab(const String& modelPath = {}, const String& modelName = {}) + { + int index = getNumTabs(); + + auto* tab = new ModelTab(); + + auto tabName = modelName; + + if (tabName.isEmpty() && modelPath.isNotEmpty()) + tabName = ModelRegistry::getEntryForPath(modelPath).displayName; + + if (tabName.isEmpty()) + tabName = "Model " + String(index); + + addLoadedModelTab(tab, tabName); + + if (modelPath.isNotEmpty()) + tab->loadModelPath(modelPath); + + return tab; + } + + ModelTab* getCurrentModelTab() const + { + return dynamic_cast(getCurrentContentComponent()); + } + + ModelTab* getFirstModelTab() const + { + for (int i = 0; i < getNumTabs(); ++i) + { + if (auto* tab = dynamic_cast(getTabContentComponent(i))) + return tab; + } + + return nullptr; + } + +private: + void addLoadedModelTab(ModelTab* tab, const String& tabName) + { + tab->addChangeListener(this); + + addTab(tabName, + tabBackgroundColour, + tab, + true); + + addCloseButtonToModelTab(tab); + + setCurrentTabIndex(getNumTabs() - 1); + } + + void addCloseButtonToModelTab(ModelTab* tab) + { + auto* closeButton = new TextButton("x"); + closeButton->setTooltip("Close model tab"); + closeButton->setSize(18, 18); + closeButton->setColour(TextButton::buttonColourId, Colours::transparentBlack); + closeButton->setColour(TextButton::buttonOnColourId, Colours::transparentBlack); + closeButton->setColour(TextButton::textColourOffId, Colours::white); + closeButton->setColour(TextButton::textColourOnId, Colours::white); + closeButton->onClick = [this, tab] { closeModelTab(tab); }; + + if (auto* tabButton = getTabbedButtonBar().getTabButton(getNumTabs() - 1)) + tabButton->setExtraComponent(closeButton, TabBarButton::afterText); + } + + void closeModelTab(ModelTab* tabToClose) + { + for (int i = 1; i < getNumTabs(); ++i) + { + if (getTabContentComponent(i) == tabToClose) + { + const auto currentIndex = getCurrentTabIndex(); + const auto targetIndex = currentIndex == i ? jmax(0, i - 1) + : (currentIndex > i ? currentIndex - 1 + : currentIndex); + + removeTab(i); + + if (getNumTabs() > 0) + setCurrentTabIndex(jlimit(0, getNumTabs() - 1, targetIndex)); + + sendChangeMessage(); + return; + } + } + } + + void createHomeTab() + { + auto* homeTab = new HomeTab(); + homeTab->onModelLoadRequested = [this, homeTab](String modelPath, String modelName) + { + auto* pendingTab = new ModelTab(); + pendingTab->onNextModelLoadComplete( + [this, homeTab, modelName](ModelTab* tab, bool wasSuccessful) + { + if (wasSuccessful) + { + addLoadedModelTab(tab, modelName); + sendChangeMessage(); + } + else + { + MessageManager::callAsync([tab] { delete tab; }); + } + + homeTab->resetSelection(); + }); + + pendingTab->loadModelPath(modelPath); + }; + + addTab("Home", + tabBackgroundColour, + homeTab, + false); + + setCurrentTabIndex(0); + } + + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (dynamic_cast(source)) + { + sendChangeMessage(); // bubble up to MainComponent + } + } + + const Colour tabBackgroundColour { + getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground) + }; + + ModelTabsLookAndFeel tabsLookAndFeel; +}; diff --git a/src/clients/Client.h b/src/clients/Client.h index 5596eb01..7f134307 100644 --- a/src/clients/Client.h +++ b/src/clients/Client.h @@ -180,7 +180,7 @@ class Client { public: Client() = default; - virtual ~Client() {}; + virtual ~Client() = default; virtual String inferHostSlashModel(String modelPath) = 0; virtual String inferEndpointPath(String modelPath) = 0; @@ -271,7 +271,11 @@ class Client String& payloadJSON, std::vector& outputFiles, LabelList& labels) = 0; - virtual OpResult cancel(String modelPath) { return OpResult::ok(); } + virtual OpResult cancel(String modelPath) + { + ignoreUnused(modelPath); + return OpResult::ok(); + } const String emptyJSONBody = R"({"data": []})"; diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index 9b77390f..b1374fdf 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -227,7 +227,7 @@ class GradioClient : public Client return OpResult::ok(); } - OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) + OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) override { String responseJSON; @@ -334,7 +334,7 @@ class GradioClient : public Client OpResult process(String modelPath, String& payloadJSON, std::vector& outputFiles, - LabelList& labels) + LabelList& labels) override { String responseJSON; @@ -428,7 +428,7 @@ class GradioClient : public Client return OpResult::ok(); } - OpResult cancel(String modelPath) + OpResult cancel(String modelPath) override { String response; diff --git a/src/clients/providers/stability/StabilityClient.h b/src/clients/providers/stability/StabilityClient.h index 15f66c39..bba18d37 100644 --- a/src/clients/providers/stability/StabilityClient.h +++ b/src/clients/providers/stability/StabilityClient.h @@ -145,7 +145,7 @@ class StabilityClient : public Client return documentationPath; } - OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) + OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) override { const char* jsonData; int jsonDataSize = 0; @@ -211,7 +211,7 @@ class StabilityClient : public Client OpResult process(String modelPath, String& payloadJSON, std::vector& outputFiles, - LabelList& labels) + LabelList& labels) override { DynamicObject::Ptr dataDict; diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..64c4b56c 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -191,7 +191,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to play playButtonInactiveInfo = MultiButton::Mode { "Play-Inactive", "Nothing to play.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontaudio::Play }; // Mode during playback stopButtonInfo = MultiButton::Mode { "Stop", @@ -213,7 +213,7 @@ void MediaDisplayComponent::initializeButtons() fontawesome::Folder }; chooseFileButtonInactiveInfo = MultiButton::Mode { "ChooseFile-Inactive", "Cannot choose file while processing.", - [this] {}, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Folder }; @@ -231,7 +231,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to save saveFileButtonInactiveInfo = MultiButton::Mode { "Save-Inactive", "Nothing to save.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Save }; saveFileButton.addMode(saveFileButtonActiveInfo); saveFileButton.addMode(saveFileButtonInactiveInfo); @@ -247,7 +247,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to copy copyFileButtonInactiveInfo = MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Copy }; copyFileButton.addMode(copyFileButtonActiveInfo); copyFileButton.addMode(copyFileButtonInactiveInfo); @@ -945,7 +945,7 @@ void MediaDisplayComponent::copyFileCallback() float MediaDisplayComponent::getPixelsPerSecond() { - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return getMediaWidth() / static_cast(visibleRange.getLength()); } @@ -957,7 +957,7 @@ float MediaDisplayComponent::getPixelsPerSecond() double MediaDisplayComponent::mediaXToTime(const float mX) { - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return static_cast(mX / getPixelsPerSecond()) + getTimeAtOrigin(); } @@ -971,7 +971,7 @@ float MediaDisplayComponent::timeToMediaX(const double t) { double t_ = jmin(getTotalLengthInSecs(), jmax(0.0, t)); - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return static_cast(t_ - getTimeAtOrigin()) * getPixelsPerSecond(); } @@ -986,7 +986,7 @@ float MediaDisplayComponent::mediaXToDisplayX(const float mX) float offsetX = 0; float visibleStartX = 0; - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { offsetX = static_cast(getTimeAtOrigin()) * getPixelsPerSecond(); visibleStartX = static_cast(visibleRange.getStart() * getPixelsPerSecond()); @@ -1347,7 +1347,7 @@ void MediaDisplayComponent::mouseUp(const MouseEvent& e) } } -void MediaDisplayComponent::mouseDoubleClick(const MouseEvent& e) +void MediaDisplayComponent::mouseDoubleClick(const MouseEvent& /*e*/) { // TODO - mouseUp/Down (selectTrack()) is still called before this @@ -1526,4 +1526,4 @@ void MediaDisplayComponent::clearLabels(int processingIdxCutoff) } resized(); // Remove overhead label panel -} \ No newline at end of file +} diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..7c118fdd 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -31,7 +31,7 @@ class ColorablePanel : public Component { public: ColorablePanel(Colour color = Colours::darkgrey) - : defaultColor(color), backgroundColor(color) {}; + : defaultColor(color), backgroundColor(color) {} void paint(Graphics& g) override { g.fillAll(backgroundColor); } @@ -312,4 +312,4 @@ class MediaDisplayComponent : public Component, SharedResourcePointer instructionsMessage; SharedResourcePointer statusMessage; -}; \ No newline at end of file +}; diff --git a/src/media/pianoroll/KeyboardComponent.hpp b/src/media/pianoroll/KeyboardComponent.hpp index 105ebea0..aeaa3e29 100644 --- a/src/media/pianoroll/KeyboardComponent.hpp +++ b/src/media/pianoroll/KeyboardComponent.hpp @@ -13,14 +13,14 @@ using namespace juce; class KeyboardComponent : public Component { public: - KeyboardComponent() {}; + KeyboardComponent() {} - ~KeyboardComponent() {}; + ~KeyboardComponent() override {} static const char* pitchNames[]; static const Array blackPitches; - void paint(Graphics& g); + void paint(Graphics& g) override; virtual bool isKeyboardComponent() { return true; } diff --git a/src/utils/Errors.h b/src/utils/Errors.h index fd9c8318..779fbc74 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -21,9 +21,9 @@ struct ClientError Type type; - String path; - String client; - String token; + String path {}; + String client {}; + String token {}; }; inline String toUserMessage(const ClientError& e) @@ -117,7 +117,7 @@ struct HttpError Request request; - String endpointPath; + String endpointPath {}; int statusCode = 0; }; @@ -226,7 +226,7 @@ struct GradioError Type type; - String endpointPath; + String endpointPath {}; }; inline String toUserMessage(const GradioError& e) @@ -266,8 +266,8 @@ struct JsonError Type type; - String stringJSON; - String key; + String stringJSON {}; + String key {}; }; inline String toUserMessage(const JsonError& e) @@ -354,7 +354,7 @@ struct ControlError Type type; - String controlType; + String controlType {}; }; inline String toUserMessage(const ControlError& e) diff --git a/src/utils/ModelRegistry.h b/src/utils/ModelRegistry.h new file mode 100644 index 00000000..71e56503 --- /dev/null +++ b/src/utils/ModelRegistry.h @@ -0,0 +1,156 @@ +/** + * @file ModelRegistry.h + * @brief Temporary model registry accessors for model discovery. + */ + +#pragma once + +#include + +#include + +using namespace juce; + +namespace ModelRegistry +{ +struct Entry +{ + String path; + String displayName; + String summary; + String provider; + std::vector tags; + + Entry() = default; + Entry(String p, String dn, String s, String pr, std::vector t = {}) + : path(std::move(p)), displayName(std::move(dn)), summary(std::move(s)), provider(std::move(pr)), tags(std::move(t)) + {} +}; + +inline String getCleanModelPath(const String& modelPath) +{ + auto cleaned = modelPath.trim(); + + for (const auto& tag : { String(" [ERROR]"), String(" [DOWN]"), String(" [TRY AGAIN]"), String(" [SLEEPING]") }) + cleaned = cleaned.replace(tag, ""); + + return cleaned.trim(); +} + +inline String getFallbackModelDisplayName(const String& modelPath) +{ + auto cleaned = getCleanModelPath(modelPath).upToFirstOccurrenceOf(" [", false, false).trim(); + auto tokens = StringArray::fromTokens(cleaned, "/", ""); + + if (tokens.size() > 0) + return tokens[tokens.size() - 1].replaceCharacter('-', ' '); + + return cleaned; +} + +inline std::vector getFeaturedModels() +{ + return { + { "stability/text-to-audio", + "Stable Audio Text to Audio", + "Generate music, sound effects, or soundscapes from a text prompt.", + "Stability AI", + { "Generation" } }, + { "stability/audio-to-audio", + "Stable Audio Audio to Audio", + "Create variations or transfer style using text and audio conditioning.", + "Stability AI", + { "Generation", "Effects" } }, + { "teamup-tech/text2midi-symbolic-music-generation", + "Text2Midi", + "Generate symbolic MIDI music from a text description.", + "Hugging Face", + { "Generation" } }, + { "teamup-tech/demucs-source-separation", + "Demucs", + "Split a music recording into drums, bass, vocals, and instrumental stems.", + "Hugging Face", + { "Source Separation" } }, + { "teamup-tech/solo-piano-audio-to-midi-transcription", + "High Resolution Piano Transcription", + "Convert solo piano audio into a corresponding MIDI performance.", + "Hugging Face", + { "Analysis" } }, + { "teamup-tech/transkun", + "Transkun", + "Transcribe musical audio into symbolic note events.", + "Hugging Face", + { "Analysis" } }, + { "teamup-tech/TRIA", + "TRIA", + "Generate drum accompaniment conditioned on rhythmic input.", + "Hugging Face", + { "Performance Rendering and Synthesis", "Generation" } }, + { "teamup-tech/anticipatory-music-transformer", + "Anticipatory Music Transformer", + "Harmonize MIDI melodies by generating musically compatible notes.", + "Hugging Face", + { "Generation" } }, + { "teamup-tech/vampnet-conditional-music-generation", + "VampNet", + "Generate controllable variations of an input music recording.", + "Hugging Face", + { "Generation", "Effects" } }, + { "teamup-tech/harmonic-percussive-separation", + "Harmonic/Percussive Separation", + "Separate audio into harmonic and percussive components.", + "Hugging Face", + { "Source Separation" } }, + { "teamup-tech/Kokoro-TTS", + "Kokoro TTS", + "Generate speech from text using a selected voice preset.", + "Hugging Face", + { "Performance Rendering and Synthesis" } }, + { "teamup-tech/MegaTTS3-Voice-Cloning", + "MegaTTS3 Voice Cloning", + "Generate speech from text conditioned on a reference voice recording.", + "Hugging Face", + { "Performance Rendering and Synthesis" } }, + { "teamup-tech/midi-synthesizer", + "MIDI Synthesizer", + "Render MIDI into audio using the standard MuseScore SoundFont.", + "Hugging Face", + { "Performance Rendering and Synthesis" } }, + { "teamup-tech/audioseal", + "AudioSeal", + "Apply or inspect audio watermarking for generated audio workflows.", + "Hugging Face", + { "Analysis", "Production" } }, + }; +} + +inline std::vector getFeaturedModelPaths() +{ + std::vector paths { "click here to enter a custom path..." }; + + for (const auto& entry : getFeaturedModels()) + paths.push_back(entry.path.toStdString()); + + return paths; +} + +inline Entry getEntryForPath(const String& modelPath) +{ + const auto cleanedPath = getCleanModelPath(modelPath).upToFirstOccurrenceOf(" [", false, false).trim(); + + for (const auto& entry : getFeaturedModels()) + { + if (entry.path == cleanedPath) + { + auto result = entry; + result.path = cleanedPath; + return result; + } + } + + return { cleanedPath, + getFallbackModelDisplayName(modelPath), + "Custom or recently used HARP-compatible model endpoint.", + cleanedPath.startsWith("stability/") ? "Stability AI" : "Custom" }; +} +} // namespace ModelRegistry diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 4dd5d940..becb98e9 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -34,9 +34,9 @@ class MediaClipboardWidget : public Component, public ChangeListener addAndMakeVisible(trackArea); } - ~MediaClipboardWidget() { trackAreaWidget.removeChangeListener(this); } + ~MediaClipboardWidget() override { trackAreaWidget.removeChangeListener(this); } - void paint(Graphics& g) { g.fillAll(Colours::lightgrey.darker().withAlpha(0.5f)); } + void paint(Graphics& g) override { g.fillAll(Colours::lightgrey.darker().withAlpha(0.5f)); } void resized() override { diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index bba39217..b43c3086 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -8,6 +8,7 @@ #include #include +#include #include @@ -19,18 +20,33 @@ #include "../utils/Errors.h" #include "../utils/Interface.h" #include "../utils/Logging.h" +#include "../utils/ModelRegistry.h" using namespace juce; struct SharedChoices : public ChangeBroadcaster { + enum class LoadStatus + { + None, + Error, + Down, + TryAgain + }; + + SharedChoices() + : savedModelPaths(ModelRegistry::getFeaturedModelPaths()) + { + } + int getIndexForPath(const std::string& p) { int idx = -1; + const auto cleanedPath = stripStatusTag(p); for (unsigned int i = 0; i < savedModelPaths.size(); ++i) { - if (savedModelPaths[i] == p) + if (savedModelPaths[i] == cleanedPath) { idx = (int) i; @@ -46,34 +62,83 @@ struct SharedChoices : public ChangeBroadcaster void addNewPath(const std::string& p) { - savedModelPaths.push_back(p); + const auto cleanedPath = stripStatusTag(p); + + if (! containsPath(cleanedPath)) + savedModelPaths.push_back(cleanedPath); + sendSynchronousChangeMessage(); } void updatePath(unsigned int idx, const std::string& p) { - savedModelPaths[idx] = p; + savedModelPaths[idx] = stripStatusTag(p); sendSynchronousChangeMessage(); } - std::vector savedModelPaths = { - "click here to enter a custom path...", - "stability/text-to-audio", - "stability/audio-to-audio", - "teamup-tech/text2midi-symbolic-music-generation", - "teamup-tech/demucs-source-separation", - "teamup-tech/solo-piano-audio-to-midi-transcription", - "teamup-tech/transkun", // TODO - more intuitive name - "teamup-tech/TRIA", // TODO - more intuitive name: (The Rhythm In Anything) conditional drum generation - "teamup-tech/anticipatory-music-transformer", - "teamup-tech/vampnet-conditional-music-generation", - "teamup-tech/harmonic-percussive-separation", - "teamup-tech/Kokoro-TTS", - "teamup-tech/MegaTTS3-Voice-Cloning", - "teamup-tech/midi-synthesizer", - "teamup-tech/audioseal", // TODO - more intuitive name - // "xribene/HARP-UI-TEST-v3" - }; + void setLoadStatus(const std::string& p, LoadStatus status) + { + const auto cleanedPath = stripStatusTag(p); + + if (! containsPath(cleanedPath)) + savedModelPaths.push_back(cleanedPath); + + if (status == LoadStatus::None) + loadStatuses.erase(cleanedPath); + else + loadStatuses[cleanedPath] = status; + + sendSynchronousChangeMessage(); + } + + String getDisplayTextForIndex(unsigned int idx) const + { + if (idx >= savedModelPaths.size()) + return {}; + + const auto& path = savedModelPaths[idx]; + const auto status = loadStatuses.find(path); + + if (status == loadStatuses.end()) + return path; + + return String(path) + getStatusTag(status->second); + } + + static std::string stripStatusTag(const std::string& p) + { + auto cleaned = String(p).trim(); + + for (const auto& tag : { errorTag, downTag, tryAgainTag }) + cleaned = cleaned.replace(String(tag), ""); + + return cleaned.trim().toStdString(); + } + + std::vector savedModelPaths; + +private: + static String getStatusTag(LoadStatus status) + { + switch (status) + { + case LoadStatus::Error: + return errorTag; + case LoadStatus::Down: + return downTag; + case LoadStatus::TryAgain: + return tryAgainTag; + case LoadStatus::None: + default: + return {}; + } + } + + inline static const String errorTag { " [ERROR]" }; + inline static const String downTag { " [DOWN]" }; + inline static const String tryAgainTag { " [TRY AGAIN]" }; + + std::map loadStatuses; }; class CustomPathComponent : public Component @@ -212,7 +277,7 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public void loadModelBypass(const String& modelPath) { - selectedPath = modelPath; + selectedPath = SharedChoices::stripStatusTag(modelPath.toStdString()); sendChangeMessage(); } @@ -247,43 +312,18 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public void setSuccessfulState() { - std::string loadedPath = selectedPath.toStdString(); + std::string loadedPath = SharedChoices::stripStatusTag(selectedPath.toStdString()); if (! sharedChoices->containsPath(loadedPath)) { - if (sharedChoices->containsPath(loadedPath + validPathBrokenTag)) - { - unsigned int currentIdx = - (unsigned int) sharedChoices->getIndexForPath(loadedPath + validPathBrokenTag); + // Add a new entry for custom path + sharedChoices->addNewPath(loadedPath); - // Remove broken tag from existing entry for path - sharedChoices->updatePath(currentIdx, loadedPath); - } - else if (sharedChoices->containsPath(loadedPath + validPathTryAgainTag)) - { - unsigned int currentIdx = (unsigned int) sharedChoices->getIndexForPath( - loadedPath + validPathTryAgainTag); - - // Remove try again tag from existing entry for path - sharedChoices->updatePath(currentIdx, loadedPath); - } - else if (sharedChoices->containsPath(loadedPath + validPathErrorTag)) - { - unsigned int currentIdx = - (unsigned int) sharedChoices->getIndexForPath(loadedPath + validPathErrorTag); - - // Remove error tag from existing entry for path - sharedChoices->updatePath(currentIdx, loadedPath); - } - else - { - // Add a new entry for custom path - sharedChoices->addNewPath(loadedPath); - - lastSelectedPathIndex = sharedChoices->getIndexForPath(loadedPath); - } + lastSelectedPathIndex = sharedChoices->getIndexForPath(loadedPath); } + sharedChoices->setLoadStatus(loadedPath, SharedChoices::LoadStatus::None); + lastLoadedPathIndex = sharedChoices->getIndexForPath(loadedPath); setFinishedState(); @@ -316,56 +356,23 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public } } - std::string originalEntry = selectedPath.toStdString(); - std::string updatedEntry = selectedPath.toStdString(); + std::string originalEntry = SharedChoices::stripStatusTag(selectedPath.toStdString()); + auto status = SharedChoices::LoadStatus::Error; if (const auto* e = std::get_if(&error)) { if (e->type == HttpError::Type::ConnectionFailed && e->request == HttpError::Request::POST) { - updatedEntry += validPathTryAgainTag; + status = SharedChoices::LoadStatus::TryAgain; } if (e->type == HttpError::Type::BadStatusCode && e->statusCode == 503) { - updatedEntry += validPathBrokenTag; + status = SharedChoices::LoadStatus::Down; } } - else - { - updatedEntry += validPathErrorTag; - } - - // Check for previously added unsuccessful tags before querying - if (sharedChoices->containsPath(originalEntry + validPathErrorTag)) - { - originalEntry += validPathErrorTag; - } - if (sharedChoices->containsPath(originalEntry + validPathBrokenTag)) - { - originalEntry += validPathBrokenTag; - } - if (sharedChoices->containsPath(originalEntry + validPathTryAgainTag)) - { - originalEntry += validPathTryAgainTag; - } - - if (sharedChoices->containsPath(updatedEntry)) - { - // Path has already been updated - } - else if (sharedChoices->containsPath(originalEntry)) - { - unsigned int currentIdx = (unsigned int) sharedChoices->getIndexForPath(originalEntry); - // Update entry with tag for existing path - sharedChoices->updatePath(currentIdx, updatedEntry); - } - else - { - // Add a new entry with tag for custom path - sharedChoices->addNewPath(updatedEntry); - } + sharedChoices->setLoadStatus(originalEntry, status); lastSelectedPathIndex = lastLoadedPathIndex; @@ -384,7 +391,8 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public for (unsigned int i = 0; i < sharedChoices->savedModelPaths.size(); ++i) { // Add saved path to combo box (skipping 0 for custom path) - modelPathComboBox.addItem(sharedChoices->savedModelPaths[i], static_cast(i) + 1); + modelPathComboBox.addItem(sharedChoices->getDisplayTextForIndex(i), + static_cast(i) + 1); } } @@ -448,22 +456,8 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public { if (modelPathComboBox.getSelectedItemIndex() != 0) { - selectedPath = modelPathComboBox.getText(); - - if (selectedPath.contains(validPathBrokenTag)) - { - selectedPath = selectedPath.replace(validPathBrokenTag, ""); - } - - if (selectedPath.contains(validPathTryAgainTag)) - { - selectedPath = selectedPath.replace(validPathTryAgainTag, ""); - } - - if (selectedPath.contains(validPathErrorTag)) - { - selectedPath = selectedPath.replace(validPathErrorTag, ""); - } + const auto selectedIndex = modelPathComboBox.getSelectedItemIndex(); + selectedPath = sharedChoices->savedModelPaths[(unsigned int) selectedIndex]; sendChangeMessage(); } @@ -480,7 +474,7 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public } /** - * Create callbacks for and launch the custom path popup. + * Create caollbacks for and launch the custom path popup. */ void openCustomPathPopup(const String& prefillText = "") { @@ -540,10 +534,6 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public int lastLoadedPathIndex; // Keep track of last loaded index for load failure cases int lastSelectedPathIndex; - const std::string validPathErrorTag = " [ERROR]"; - const std::string validPathBrokenTag = " [DOWN]"; - const std::string validPathTryAgainTag = " [TRY AGAIN]"; - String selectedPath; MultiButton loadModelButton; diff --git a/src/widgets/StatusAreaWidget.h b/src/widgets/StatusAreaWidget.h index 3c7189a3..27dc27d6 100644 --- a/src/widgets/StatusAreaWidget.h +++ b/src/widgets/StatusAreaWidget.h @@ -40,7 +40,7 @@ class MessageBox : public Component, ChangeListener public: MessageBox(float fontSize = 15.0f, Justification justification = Justification::centred) { - messageLabel.setFont(fontSize); + messageLabel.setFont(FontOptions { fontSize }); messageLabel.setColour(Label::textColourId, Colour(0xE0, 0xE0, 0xE0)); messageLabel.setJustificationType(justification); @@ -51,7 +51,7 @@ class MessageBox : public Component, ChangeListener ~MessageBox() override { sharedMessage->removeChangeListener(this); } - void paint(Graphics& g) + void paint(Graphics& g) override { g.setColour(Colour(0x33, 0x33, 0x33)); g.fillAll(); @@ -60,9 +60,9 @@ class MessageBox : public Component, ChangeListener g.drawRect(getLocalBounds(), 1); } - void resized() { messageLabel.setBounds(getLocalBounds()); } + void resized() override { messageLabel.setBounds(getLocalBounds()); } - void changeListenerCallback(ChangeBroadcaster* /*source*/) + void changeListenerCallback(ChangeBroadcaster* /*source*/) override { messageLabel.setText(sharedMessage->message, dontSendNotification); } @@ -84,7 +84,7 @@ class StatusAreaWidget : public Component addAndMakeVisible(statusBox); } - ~StatusAreaWidget() {} + ~StatusAreaWidget() override {} void resized() override { diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ad3d7188..7450f6d5 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -54,7 +54,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - mainComponent->getModelTab()->addChangeListener(this); + if (auto* tab = mainComponent->getFirstModelTab()) + tab->addChangeListener(this); mainComponent->setTutorialActive(true); } @@ -66,7 +67,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent) { - mainComponent->getModelTab()->removeChangeListener(this); + if (auto* tab = mainComponent->getFirstModelTab()) + tab->removeChangeListener(this); mainComponent->setTutorialActive(false); } } @@ -78,7 +80,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; auto loadedPath = model ? model->getLoadedPath() : String(); autoLoadedByTutorialFallback = (loadedPath == TutorialConstants::fallbackModelPath); @@ -87,7 +90,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } else if (autoLoadedByTutorialFallback && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; auto loadedPath = model ? model->getLoadedPath() : String(); if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; @@ -134,7 +138,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (model && model->isLoaded()) { modelName = model->getMetadata().name; @@ -178,7 +183,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // 4. Configure Parameters (Dynamic) if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (model && model->isLoaded()) { String controlsStepTitle = "Configure Parameters (Optional)"; @@ -714,7 +720,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (content->currentStep == 1 && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true;