From 9d7028eebd8de9fc29aa4d98a4f025fb7ccb7323 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Wed, 29 Apr 2026 11:48:58 -0400 Subject: [PATCH 1/9] First Draft for MultiTabs in HARP --- CMakeLists.txt | 1 + src/MainComponent.cpp | 132 ++++++++++++++++++++++------- src/MainComponent.h | 10 ++- src/ModelTab.h | 2 +- src/widgets/ModelSelectionWidget.h | 2 +- src/windows/WelcomeWindow.h | 14 +-- 6 files changed, 119 insertions(+), 42 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..3f01d933 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ target_sources(${PROJECT_NAME} src/Main.cpp src/Application.cpp src/MainComponent.cpp + src/ModelTabContainer.h src/ModelTab.h src/Model.h diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index a31a32df..cdd17b63 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -10,12 +10,19 @@ MainComponent::MainComponent() initializeMenuBar(); - mainModelTab.addChangeListener(this); + modelTabs.addChangeListener(this); - addAndMakeVisible(mainModelTab); + addAndMakeVisible(modelTabs); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); + addAndMakeVisible(addTabButton); + + addTabButton.onClick = [this] + { + modelTabs.createNewTab(); + }; + showStatusArea = Settings::getBoolValue("view.showStatusArea", true); showMediaClipboard = Settings::getBoolValue("view.showMediaClipboard", false); @@ -31,7 +38,7 @@ MainComponent::MainComponent() MainComponent::~MainComponent() { deinitializeMenuBar(); - mainModelTab.removeChangeListener(this); + modelTabs.removeChangeListener(this); } void MainComponent::paint(Graphics& g) @@ -83,6 +90,11 @@ void MainComponent::paintOverChildren(Graphics& g) } } +ModelTab* MainComponent::getCurrentModelTab() const +{ + return modelTabs.getCurrentModelTab(); +} + void MainComponent::resized() { Rectangle fullArea = getLocalBounds(); @@ -92,13 +104,31 @@ 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); + + // Get tab bar height + int tabBarHeight = modelTabs.getTabBarDepth(); + + // Position "+" button inside tab bar + addTabButton.setBounds( + bounds.getRight() - 35, // right edge + bounds.getY() + 2, // small padding from top + 30, + tabBarHeight - 4 // match tab height nicely + ); if (showStatusArea) { @@ -128,8 +158,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 +173,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 +443,97 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::ensureTutorialModelLoaded() { - if (! mainModelTab.isModelLoaded()) - mainModelTab.loadDefaultModel(); +if (auto* tab = getCurrentModelTab()) +{ + if (!tab->isModelLoaded()) + tab->loadDefaultModel(); +} } void MainComponent::resetTutorialAutoLoadedModel() { - if (! mainModelTab.isModelLoaded()) + if (auto* tab = getCurrentModelTab()) +{ + if (!tab->isModelLoaded()) return; - - if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) +} + if (auto* tab = getCurrentModelTab()) +{ + if (tab->getLoadedPath() == TutorialConstants::fallbackModelPath) { - mainModelTab.resetState(); + tab->resetState(); } } +} Rectangle MainComponent::getModelSelectBounds() { - auto bounds = mainModelTab.getModelSelectBounds(); - return getLocalArea(&mainModelTab, 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 +679,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..f9a8caf9 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,15 @@ 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; + + // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); Rectangle getControlsBounds(); @@ -150,7 +152,8 @@ class MainComponent : public Component, bool showStatusArea; bool showMediaClipboard; - ModelTab mainModelTab; + + ModelTabContainer modelTabs; StatusAreaWidget statusAreaWidget; MediaClipboardWidget mediaClipboardWidget; @@ -158,6 +161,7 @@ class MainComponent : public Component, Rectangle tutorialHighlightRect; std::vector> tutorialExtraHighlights; std::unique_ptr welcomeWindow; + juce::TextButton addTabButton { "+" }; SharedResourcePointer sharedTokens; SharedResourcePointer statusMessage; diff --git a/src/ModelTab.h b/src/ModelTab.h index 3c172ede..e4bf0713 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -21,7 +21,7 @@ using namespace juce; -class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster +class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster { public: ModelTab() diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index bba39217..b5ffe518 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -480,7 +480,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 = "") { diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ad3d7188..ed905f90 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -54,7 +54,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - mainComponent->getModelTab()->addChangeListener(this); + mainComponent->getCurrentModelTab()->addChangeListener(this); mainComponent->setTutorialActive(true); } @@ -66,7 +66,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent) { - mainComponent->getModelTab()->removeChangeListener(this); + mainComponent->getCurrentModelTab()->removeChangeListener(this); mainComponent->setTutorialActive(false); } } @@ -78,7 +78,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); autoLoadedByTutorialFallback = (loadedPath == TutorialConstants::fallbackModelPath); @@ -87,7 +87,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } else if (autoLoadedByTutorialFallback && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; @@ -134,7 +134,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (model && model->isLoaded()) { modelName = model->getMetadata().name; @@ -178,7 +178,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // 4. Configure Parameters (Dynamic) if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (model && model->isLoaded()) { String controlsStepTitle = "Configure Parameters (Optional)"; @@ -714,7 +714,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (content->currentStep == 1 && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto model = mainComponent->getCurrentModelTab()->getModel(); if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true; From cea85ba370ef9ca07e748860b3b277b80b0ca031 Mon Sep 17 00:00:00 2001 From: JEYuhas Date: Wed, 29 Apr 2026 12:00:38 -0400 Subject: [PATCH 2/9] Add ModelTabContainer class for multi-tab support --- src/ModelTabContainer.h | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/ModelTabContainer.h diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h new file mode 100644 index 00000000..97b7275b --- /dev/null +++ b/src/ModelTabContainer.h @@ -0,0 +1,62 @@ +/** + * @brief Adds tab container to HARP for MultiTabs + * @author JEYuhas + */ +#pragma once + +#include + +#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/Logging.h" +#include "utils/Tutorial.h" + +using namespace juce; + +class ModelTabContainer : public TabbedComponent, + private ChangeListener, + public ChangeBroadcaster +{ +public: + ModelTabContainer() + : TabbedComponent(TabbedButtonBar::TabsAtTop) + { + createNewTab(); // start with one + } + + void createNewTab() + { + int index = getNumTabs() + 1; + + auto* tab = new ModelTab(); + tab->addChangeListener(this); + + addTab("Model " + String(index), + Colours::lightgrey, + tab, + true); + + setCurrentTabIndex(index - 1); + } + + ModelTab* getCurrentModelTab() const + { + return dynamic_cast(getCurrentContentComponent()); + } + +private: + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (dynamic_cast(source)) + { + sendChangeMessage(); // bubble up to MainComponent + } + } +}; From 71e9866fb30d1fbe696b7a39ebb8a96b3d04a327 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 26 May 2026 10:31:20 -0400 Subject: [PATCH 3/9] Add home tab model loading flow --- CMakeLists.txt | 1 + src/HomeTab.h | 76 +++++++++++++++++++++++++++++++++++++++++ src/ModelTab.h | 16 ++++----- src/ModelTabContainer.h | 31 ++++++++++++++--- 4 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/HomeTab.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f01d933..8f434b45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ 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 diff --git a/src/HomeTab.h b/src/HomeTab.h new file mode 100644 index 00000000..4ac9dcf1 --- /dev/null +++ b/src/HomeTab.h @@ -0,0 +1,76 @@ +/** + * @file HomeTab.h + * @brief Home tab for model discovery and loading. + */ + +#pragma once + +#include + +#include + +#include "widgets/ModelSelectionWidget.h" + +using namespace juce; + +class HomeTab : public Component, + private ChangeListener +{ +public: + HomeTab() + { + modelSelectionWidget.addChangeListener(this); + + titleLabel.setText("Models", dontSendNotification); + titleLabel.setJustificationType(Justification::centredLeft); + titleLabel.setFont(Font(24.0f, Font::bold)); + + subtitleLabel.setText("Select a HARP-compatible model to open it in a new tab.", + dontSendNotification); + subtitleLabel.setJustificationType(Justification::centredLeft); + + addAndMakeVisible(titleLabel); + addAndMakeVisible(subtitleLabel); + addAndMakeVisible(modelSelectionWidget); + } + + ~HomeTab() override + { + modelSelectionWidget.removeChangeListener(this); + } + + void resized() override + { + auto area = getLocalBounds().reduced(16); + + titleLabel.setBounds(area.removeFromTop(34)); + subtitleLabel.setBounds(area.removeFromTop(26)); + + area.removeFromTop(8); + modelSelectionWidget.setBounds(area.removeFromTop(34)); + } + + void resetSelection() + { + modelSelectionWidget.resetState(); + } + + std::function onModelLoadRequested; + +private: + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (source == &modelSelectionWidget) + { + const auto selectedPath = modelSelectionWidget.getCurrentlySelectedPath(); + modelSelectionWidget.setDisabled(); + + if (onModelLoadRequested) + onModelLoadRequested(selectedPath); + } + } + + Label titleLabel; + Label subtitleLabel; + ModelSelectionWidget modelSelectionWidget; +}; diff --git a/src/ModelTab.h b/src/ModelTab.h index e4bf0713..e790eff1 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -28,7 +28,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas { modelSelectionWidget.addChangeListener(this); - addAndMakeVisible(modelSelectionWidget); addAndMakeVisible(modelInfoWidget); addAndMakeVisible(controlAreaWidget); @@ -58,6 +57,11 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas modelSelectionWidget.loadModelBypass(TutorialConstants::fallbackModelPath); } + void loadModelPath(const String& modelPath) + { + modelSelectionWidget.loadModelBypass(modelPath); + } + // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const { @@ -115,12 +119,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 +197,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) @@ -609,4 +607,4 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool processingThreadPool { 10 }; std::atomic currentProcessID { 0 }; -}; \ No newline at end of file +}; diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h index 97b7275b..936a528e 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -6,6 +6,7 @@ #include +#include "HomeTab.h" #include "Model.h" #include "ModelTab.h" @@ -28,12 +29,12 @@ class ModelTabContainer : public TabbedComponent, ModelTabContainer() : TabbedComponent(TabbedButtonBar::TabsAtTop) { - createNewTab(); // start with one + createHomeTab(); } - void createNewTab() + ModelTab* createNewTab(const String& modelPath = {}) { - int index = getNumTabs() + 1; + int index = getNumTabs(); auto* tab = new ModelTab(); tab->addChangeListener(this); @@ -43,7 +44,12 @@ class ModelTabContainer : public TabbedComponent, tab, true); - setCurrentTabIndex(index - 1); + setCurrentTabIndex(getNumTabs() - 1); + + if (modelPath.isNotEmpty()) + tab->loadModelPath(modelPath); + + return tab; } ModelTab* getCurrentModelTab() const @@ -52,6 +58,23 @@ class ModelTabContainer : public TabbedComponent, } private: + void createHomeTab() + { + auto* homeTab = new HomeTab(); + homeTab->onModelLoadRequested = [this, homeTab](String modelPath) + { + createNewTab(modelPath); + homeTab->resetSelection(); + }; + + addTab("Home", + Colours::lightgrey, + homeTab, + false); + + setCurrentTabIndex(0); + } + void changeListenerCallback(ChangeBroadcaster* source) override { if (dynamic_cast(source)) From 3b23fe809ddb19bd009460449877857a402e3a41 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 26 May 2026 11:49:06 -0400 Subject: [PATCH 4/9] Improve home tab model loading flow --- src/HomeTab.h | 5 ++++ src/MainComponent.cpp | 60 ++++++++++++++++++------------------- src/MainComponent.h | 16 +++++----- src/ModelTabContainer.h | 53 ++++++++++++++++++++++++++++++-- src/windows/WelcomeWindow.h | 21 ++++++++----- 5 files changed, 107 insertions(+), 48 deletions(-) diff --git a/src/HomeTab.h b/src/HomeTab.h index 4ac9dcf1..1ef34b2f 100644 --- a/src/HomeTab.h +++ b/src/HomeTab.h @@ -55,6 +55,11 @@ class HomeTab : public Component, modelSelectionWidget.resetState(); } + Rectangle getModelSelectBounds() const + { + return modelSelectionWidget.getBounds().expanded(2, 2); + } + std::function onModelLoadRequested; private: diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index cdd17b63..d0d2600c 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -16,13 +16,6 @@ MainComponent::MainComponent() addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); - addAndMakeVisible(addTabButton); - - addTabButton.onClick = [this] - { - modelTabs.createNewTab(); - }; - showStatusArea = Settings::getBoolValue("view.showStatusArea", true); showMediaClipboard = Settings::getBoolValue("view.showMediaClipboard", false); @@ -95,6 +88,11 @@ ModelTab* MainComponent::getCurrentModelTab() const return modelTabs.getCurrentModelTab(); } +ModelTab* MainComponent::getFirstModelTab() const +{ + return modelTabs.getFirstModelTab(); +} + void MainComponent::resized() { Rectangle fullArea = getLocalBounds(); @@ -119,17 +117,6 @@ void MainComponent::resized() // Give full area to tabs modelTabs.setBounds(bounds); - // Get tab bar height - int tabBarHeight = modelTabs.getTabBarDepth(); - - // Position "+" button inside tab bar - addTabButton.setBounds( - bounds.getRight() - 35, // right edge - bounds.getY() + 2, // small padding from top - 30, - tabBarHeight - 4 // match tab height nicely - ); - if (showStatusArea) { mainPanel.items.add(FlexItem(statusAreaWidget).withHeight(statusAreaHeight)); @@ -443,31 +430,42 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::ensureTutorialModelLoaded() { -if (auto* tab = getCurrentModelTab()) -{ - if (!tab->isModelLoaded()) + 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 (auto* tab = getCurrentModelTab()) -{ - if (!tab->isModelLoaded()) - return; -} - if (auto* tab = getCurrentModelTab()) -{ - if (tab->getLoadedPath() == TutorialConstants::fallbackModelPath) { - tab->resetState(); + if (tab->isModelLoaded() && tab->getLoadedPath() == TutorialConstants::fallbackModelPath) + tab->resetState(); } } -} Rectangle MainComponent::getModelSelectBounds() { + if (auto* homeTab = dynamic_cast(modelTabs.getCurrentContentComponent())) + { + auto bounds = homeTab->getModelSelectBounds(); + return getLocalArea(homeTab, bounds); + } + if (auto* tab = getCurrentModelTab()) { auto bounds = tab->getModelSelectBounds(); diff --git a/src/MainComponent.h b/src/MainComponent.h index f9a8caf9..710122c8 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -81,7 +81,8 @@ class MainComponent : public Component, void ensureTutorialModelLoaded(); void resetTutorialAutoLoadedModel(); - ModelTab* getCurrentModelTab() const; + ModelTab* getCurrentModelTab() const; + ModelTab* getFirstModelTab() const; // Bounds accessors for tutorial steps (public for WelcomeWindow) @@ -157,13 +158,12 @@ class MainComponent : public Component, StatusAreaWidget statusAreaWidget; MediaClipboardWidget mediaClipboardWidget; - bool isTutorialActive = false; - Rectangle tutorialHighlightRect; - std::vector> tutorialExtraHighlights; - std::unique_ptr welcomeWindow; - juce::TextButton addTabButton { "+" }; - - 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/ModelTabContainer.h b/src/ModelTabContainer.h index 936a528e..4ce935cd 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -29,6 +29,12 @@ class ModelTabContainer : public TabbedComponent, ModelTabContainer() : TabbedComponent(TabbedButtonBar::TabsAtTop) { + 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(); } @@ -40,10 +46,12 @@ class ModelTabContainer : public TabbedComponent, tab->addChangeListener(this); addTab("Model " + String(index), - Colours::lightgrey, + tabBackgroundColour, tab, true); + addCloseButtonToModelTab(tab); + setCurrentTabIndex(getNumTabs() - 1); if (modelPath.isNotEmpty()) @@ -57,7 +65,46 @@ class ModelTabContainer : public TabbedComponent, 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 addCloseButtonToModelTab(ModelTab* tab) + { + auto* closeButton = new TextButton("x"); + closeButton->setTooltip("Close model tab"); + closeButton->setSize(18, 18); + closeButton->setColour(TextButton::buttonColourId, tabBackgroundColour); + closeButton->setColour(TextButton::buttonOnColourId, tabBackgroundColour.brighter(0.1f)); + 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) + { + removeTab(i); + sendChangeMessage(); + return; + } + } + } + void createHomeTab() { auto* homeTab = new HomeTab(); @@ -68,7 +115,7 @@ class ModelTabContainer : public TabbedComponent, }; addTab("Home", - Colours::lightgrey, + tabBackgroundColour, homeTab, false); @@ -82,4 +129,6 @@ class ModelTabContainer : public TabbedComponent, sendChangeMessage(); // bubble up to MainComponent } } + + const Colour tabBackgroundColour { Colour(0xff4a4a4a) }; }; diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ed905f90..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->getCurrentModelTab()->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->getCurrentModelTab()->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->getCurrentModelTab()->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->getCurrentModelTab()->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->getCurrentModelTab()->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->getCurrentModelTab()->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->getCurrentModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true; From 4aada19bcde7d0a619d478b053ecb667b254850c Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 26 May 2026 12:14:35 -0400 Subject: [PATCH 5/9] Restore background color for model UI and clean up warnings --- src/MainComponent.h | 2 +- src/ModelTabContainer.h | 5 ++++- src/clients/Client.h | 8 ++++++-- src/clients/GradioClient.h | 6 +++--- .../providers/stability/StabilityClient.h | 4 ++-- src/media/MediaDisplayComponent.cpp | 20 +++++++++---------- src/media/MediaDisplayComponent.h | 4 ++-- src/media/pianoroll/KeyboardComponent.hpp | 6 +++--- src/utils/Errors.h | 16 +++++++-------- src/widgets/MediaClipboardWidget.h | 4 ++-- src/widgets/StatusAreaWidget.h | 10 +++++----- 11 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/MainComponent.h b/src/MainComponent.h index 710122c8..4d777918 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -129,7 +129,7 @@ class MainComponent : public Component, // Miscellaneous //void focusCallback(); - void changeListenerCallback(ChangeBroadcaster* source); + void changeListenerCallback(ChangeBroadcaster* source) override; /* Interface */ diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h index 4ce935cd..b75f8c38 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -16,6 +16,7 @@ #include "widgets/TrackAreaWidget.h" #include "utils/Errors.h" +#include "utils/Interface.h" #include "utils/Logging.h" #include "utils/Tutorial.h" @@ -130,5 +131,7 @@ class ModelTabContainer : public TabbedComponent, } } - const Colour tabBackgroundColour { Colour(0xff4a4a4a) }; + const Colour tabBackgroundColour { + getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground) + }; }; 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/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/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 { From 3fe8f370ac5da08f3ba93caec737c781197f1eb2 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 26 May 2026 16:46:21 -0400 Subject: [PATCH 6/9] Replace dropdown with a model browser and search bar --- .gitignore | 1 + CMakeLists.txt | 1 + src/HomeTab.h | 230 +++++++++++++++++++++++++++-- src/ModelTabContainer.h | 17 ++- src/utils/ModelRegistry.h | 126 ++++++++++++++++ src/widgets/ModelSelectionWidget.h | 25 +--- 6 files changed, 364 insertions(+), 36 deletions(-) create mode 100644 src/utils/ModelRegistry.h 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 8f434b45..a31c1439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,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 index 1ef34b2f..cd1b2e90 100644 --- a/src/HomeTab.h +++ b/src/HomeTab.h @@ -6,37 +6,165 @@ #pragma once #include +#include +#include #include +#include "utils/Interface.h" +#include "utils/ModelRegistry.h" #include "widgets/ModelSelectionWidget.h" using namespace juce; +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); + } + + 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)); + + providerLabel.setBounds(area.removeFromTop(18)); + 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; +}; + +class ModelRegistryList : public Component +{ +public: + void setEntries(std::vector newEntries, + std::function loadCallback) + { + cards.clear(); + removeAllChildren(); + + for (auto& entry : newEntries) + { + auto card = std::make_unique(std::move(entry), loadCallback); + addAndMakeVisible(*card); + cards.push_back(std::move(card)); + } + + resized(); + repaint(); + } + + void resized() override + { + auto area = getLocalBounds(); + + for (auto& card : cards) + card->setBounds(area.removeFromTop(ModelRegistryCard::preferredHeight).reduced(0, 4)); + } + + int getRequiredHeight() const + { + return static_cast(cards.size()) * ModelRegistryCard::preferredHeight; + } + +private: + std::vector> cards; +}; + class HomeTab : public Component, private ChangeListener { public: HomeTab() { - modelSelectionWidget.addChangeListener(this); + sharedChoices->addChangeListener(this); titleLabel.setText("Models", dontSendNotification); titleLabel.setJustificationType(Justification::centredLeft); titleLabel.setFont(Font(24.0f, Font::bold)); - subtitleLabel.setText("Select a HARP-compatible model to open it in a new tab.", + 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(modelSelectionWidget); + addAndMakeVisible(searchEditor); + addAndMakeVisible(customPathButton); + addAndMakeVisible(viewport); + + rebuildModelList(); } ~HomeTab() override { - modelSelectionWidget.removeChangeListener(this); + sharedChoices->removeChangeListener(this); } void resized() override @@ -47,35 +175,109 @@ class HomeTab : public Component, subtitleLabel.setBounds(area.removeFromTop(26)); area.removeFromTop(8); - modelSelectionWidget.setBounds(area.removeFromTop(34)); + auto searchRow = area.removeFromTop(34); + customPathButton.setBounds(searchRow.removeFromRight(120).reduced(0, 1)); + searchRow.removeFromRight(8); + searchEditor.setBounds(searchRow); + + area.removeFromTop(10); + viewport.setBounds(area); + + updateListBounds(); } void resetSelection() { - modelSelectionWidget.resetState(); + searchEditor.setEnabled(true); + customPathButton.setEnabled(true); + viewport.setEnabled(true); } Rectangle getModelSelectBounds() const { - return modelSelectionWidget.getBounds().expanded(2, 2); + return searchEditor.getBounds().expanded(2, 2); } - std::function onModelLoadRequested; + std::function onModelLoadRequested; private: void changeListenerCallback(ChangeBroadcaster* source) override { - if (source == &modelSelectionWidget) + if (source == static_cast(sharedChoices)) + rebuildModelList(); + } + + void requestModelLoad(const ModelRegistry::Entry& entry) + { + searchEditor.setEnabled(false); + customPathButton.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 auto selectedPath = modelSelectionWidget.getCurrentlySelectedPath(); - modelSelectionWidget.setDisabled(); + 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 (onModelLoadRequested) - onModelLoadRequested(selectedPath); + if (searchText.isEmpty() || searchableText.contains(searchText)) + entries.push_back(std::move(entry)); } + + modelList.setEntries(std::move(entries), + [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; - ModelSelectionWidget modelSelectionWidget; + TextEditor searchEditor; + TextButton customPathButton; + Viewport viewport; + ModelRegistryList modelList; + + SharedResourcePointer sharedChoices; }; diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h index b75f8c38..a374844e 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -18,6 +18,7 @@ #include "utils/Errors.h" #include "utils/Interface.h" #include "utils/Logging.h" +#include "utils/ModelRegistry.h" #include "utils/Tutorial.h" using namespace juce; @@ -39,14 +40,22 @@ class ModelTabContainer : public TabbedComponent, createHomeTab(); } - ModelTab* createNewTab(const String& modelPath = {}) + ModelTab* createNewTab(const String& modelPath = {}, const String& modelName = {}) { int index = getNumTabs(); auto* tab = new ModelTab(); tab->addChangeListener(this); - addTab("Model " + String(index), + auto tabName = modelName; + + if (tabName.isEmpty() && modelPath.isNotEmpty()) + tabName = ModelRegistry::getEntryForPath(modelPath).displayName; + + if (tabName.isEmpty()) + tabName = "Model " + String(index); + + addTab(tabName, tabBackgroundColour, tab, true); @@ -109,9 +118,9 @@ class ModelTabContainer : public TabbedComponent, void createHomeTab() { auto* homeTab = new HomeTab(); - homeTab->onModelLoadRequested = [this, homeTab](String modelPath) + homeTab->onModelLoadRequested = [this, homeTab](String modelPath, String modelName) { - createNewTab(modelPath); + createNewTab(modelPath, modelName); homeTab->resetSelection(); }; diff --git a/src/utils/ModelRegistry.h b/src/utils/ModelRegistry.h new file mode 100644 index 00000000..90f7e922 --- /dev/null +++ b/src/utils/ModelRegistry.h @@ -0,0 +1,126 @@ +/** + * @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; +}; + +inline String getFallbackModelDisplayName(const String& modelPath) +{ + auto cleaned = 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" }, + { "stability/audio-to-audio", + "Stable Audio Audio to Audio", + "Create variations or transfer style using text and audio conditioning.", + "Stability AI" }, + { "teamup-tech/text2midi-symbolic-music-generation", + "Text2Midi", + "Generate symbolic MIDI music from a text description.", + "Hugging Face" }, + { "teamup-tech/demucs-source-separation", + "Demucs", + "Split a music recording into drums, bass, vocals, and instrumental stems.", + "Hugging Face" }, + { "teamup-tech/solo-piano-audio-to-midi-transcription", + "High Resolution Piano Transcription", + "Convert solo piano audio into a corresponding MIDI performance.", + "Hugging Face" }, + { "teamup-tech/transkun", + "Transkun", + "Transcribe musical audio into symbolic note events.", + "Hugging Face" }, + { "teamup-tech/TRIA", + "TRIA", + "Generate drum accompaniment conditioned on rhythmic input.", + "Hugging Face" }, + { "teamup-tech/anticipatory-music-transformer", + "Anticipatory Music Transformer", + "Harmonize MIDI melodies by generating musically compatible notes.", + "Hugging Face" }, + { "teamup-tech/vampnet-conditional-music-generation", + "VampNet", + "Generate controllable variations of an input music recording.", + "Hugging Face" }, + { "teamup-tech/harmonic-percussive-separation", + "Harmonic/Percussive Separation", + "Separate audio into harmonic and percussive components.", + "Hugging Face" }, + { "teamup-tech/Kokoro-TTS", + "Kokoro TTS", + "Generate speech from text using a selected voice preset.", + "Hugging Face" }, + { "teamup-tech/MegaTTS3-Voice-Cloning", + "MegaTTS3 Voice Cloning", + "Generate speech from text conditioned on a reference voice recording.", + "Hugging Face" }, + { "teamup-tech/midi-synthesizer", + "MIDI Synthesizer", + "Render MIDI into audio using the standard MuseScore SoundFont.", + "Hugging Face" }, + { "teamup-tech/audioseal", + "AudioSeal", + "Apply or inspect audio watermarking for generated audio workflows.", + "Hugging Face" }, + }; +} + +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 = modelPath.upToFirstOccurrenceOf(" [", false, false).trim(); + + for (const auto& entry : getFeaturedModels()) + { + if (entry.path == cleanedPath) + { + auto result = entry; + result.path = modelPath; + return result; + } + } + + return { modelPath, + getFallbackModelDisplayName(modelPath), + "Custom or recently used HARP-compatible model endpoint.", + cleanedPath.startsWith("stability/") ? "Stability AI" : "Custom" }; +} +} // namespace ModelRegistry diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index b5ffe518..82bed1e1 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -19,11 +19,17 @@ #include "../utils/Errors.h" #include "../utils/Interface.h" #include "../utils/Logging.h" +#include "../utils/ModelRegistry.h" using namespace juce; struct SharedChoices : public ChangeBroadcaster { + SharedChoices() + : savedModelPaths(ModelRegistry::getFeaturedModelPaths()) + { + } + int getIndexForPath(const std::string& p) { int idx = -1; @@ -56,24 +62,7 @@ struct SharedChoices : public ChangeBroadcaster 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" - }; + std::vector savedModelPaths; }; class CustomPathComponent : public Component From ce29c24537df3b62564e10ca868b442f80a51e80 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 2 Jun 2026 15:51:45 -0400 Subject: [PATCH 7/9] Edited tab appearance --- src/ModelTabContainer.h | 96 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h index a374844e..602fa44e 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -23,6 +23,80 @@ 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 @@ -31,6 +105,8 @@ class ModelTabContainer : public TabbedComponent, ModelTabContainer() : TabbedComponent(TabbedButtonBar::TabsAtTop) { + getTabbedButtonBar().setLookAndFeel(&tabsLookAndFeel); + setColour(TabbedComponent::backgroundColourId, tabBackgroundColour); getTabbedButtonBar().setColour(TabbedButtonBar::tabTextColourId, Colours::white); getTabbedButtonBar().setColour(TabbedButtonBar::frontTextColourId, Colours::white); @@ -40,6 +116,11 @@ class ModelTabContainer : public TabbedComponent, createHomeTab(); } + ~ModelTabContainer() override + { + getTabbedButtonBar().setLookAndFeel(nullptr); + } + ModelTab* createNewTab(const String& modelPath = {}, const String& modelName = {}) { int index = getNumTabs(); @@ -92,8 +173,8 @@ class ModelTabContainer : public TabbedComponent, auto* closeButton = new TextButton("x"); closeButton->setTooltip("Close model tab"); closeButton->setSize(18, 18); - closeButton->setColour(TextButton::buttonColourId, tabBackgroundColour); - closeButton->setColour(TextButton::buttonOnColourId, tabBackgroundColour.brighter(0.1f)); + 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); }; @@ -108,7 +189,16 @@ class ModelTabContainer : public TabbedComponent, { 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; } @@ -143,4 +233,6 @@ class ModelTabContainer : public TabbedComponent, const Colour tabBackgroundColour { getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground) }; + + ModelTabsLookAndFeel tabsLookAndFeel; }; From 944d49ce8c32e6633dc0068425a4b49a6cbeda67 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Fri, 12 Jun 2026 10:58:03 -0400 Subject: [PATCH 8/9] Add model taxonomy categorization --- src/HomeTab.h | 346 ++++++++++++++++++++++++++++++++++++-- src/utils/ModelRegistry.h | 48 ++++-- 2 files changed, 366 insertions(+), 28 deletions(-) diff --git a/src/HomeTab.h b/src/HomeTab.h index cd1b2e90..8e4e554c 100644 --- a/src/HomeTab.h +++ b/src/HomeTab.h @@ -17,6 +17,198 @@ using namespace juce; +class TagLabel : public Component +{ +public: + TagLabel(const String& text) : tagText(text) + { + setSize(font.getStringWidth(tagText) + 12, 18); + } + + void paint(Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(0.5f); + g.setColour(Colour(0xff2d2d35)); + g.fillRoundedRectangle(bounds, 4.0f); + g.setColour(Colour(0xff4f46e5).withAlpha(0.4f)); + g.drawRoundedRectangle(bounds, 4.0f, 1.0f); + + g.setColour(Colour(0xffa5b4fc)); + g.setFont(font); + g.drawText(tagText, getLocalBounds(), Justification::centred, true); + } + +private: + String tagText; + Font font { 10.0f, Font::bold }; +}; + +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; } + + void paintButton(Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + + Colour bg; + Colour textColour; + + if (isSelected) + { + bg = Colour(0xff4f46e5); + textColour = Colours::white; + } + else if (shouldDrawButtonAsHighlighted || shouldDrawButtonAsDown) + { + bg = Colour(0xff2d2d30); + textColour = Colours::white; + } + else + { + bg = Colour(0xff1e1e24); + textColour = Colours::lightgrey; + } + + g.setColour(bg); + g.fillRoundedRectangle(bounds, bounds.getHeight() * 0.5f); + + g.setColour(isSelected ? Colour(0xff818cf8) : Colours::white.withAlpha(0.1f)); + g.drawRoundedRectangle(bounds, bounds.getHeight() * 0.5f, 1.0f); + + g.setColour(textColour); + g.setFont(Font(13.0f, Font::bold)); + g.drawText(getName(), getLocalBounds(), Justification::centred, true); + } + +private: + bool isSelected = false; +}; + +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 rowHeight = 28; + int spacingX = 6; + int spacingY = 6; + + for (auto& chip : chips) + { + int chipWidth = Font(13.0f, Font::bold).getStringWidth(chip->getName()) + 24; + + if (x + chipWidth > area.getWidth() && x > 0) + { + x = 0; + y += rowHeight + spacingY; + } + + chip->setBounds(x, y, chipWidth, rowHeight); + x += chipWidth + spacingX; + } + + int newHeight = y + rowHeight; + 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(0xff4f46e5).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: @@ -51,6 +243,13 @@ class ModelRegistryCard : public Component 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 @@ -70,7 +269,14 @@ class ModelRegistryCard : public Component auto buttonArea = area.removeFromRight(92); loadButton.setBounds(buttonArea.withSizeKeepingCentre(80, 30)); - providerLabel.setBounds(area.removeFromTop(18)); + auto topRow = area.removeFromTop(18); + providerLabel.setBounds(topRow.removeFromLeft(150)); + + for (auto& tagLabel : tagLabels) + { + tagLabel->setBounds(topRow.removeFromRight(tagLabel->getWidth() + 4).reduced(0, 1)); + } + nameLabel.setBounds(area.removeFromTop(24)); summaryLabel.setBounds(area.removeFromTop(24)); pathLabel.setBounds(area.removeFromTop(18)); @@ -87,22 +293,39 @@ class ModelRegistryCard : public Component Label summaryLabel; Label pathLabel; TextButton loadButton; + std::vector> tagLabels; }; class ModelRegistryList : public Component { public: - void setEntries(std::vector newEntries, - std::function loadCallback) + struct Section { - cards.clear(); + String category; + std::vector entries; + }; + + void setSections(std::vector
newSections, + std::function loadCallback) + { + items.clear(); removeAllChildren(); - for (auto& entry : newEntries) + for (auto& sec : newSections) { - auto card = std::make_unique(std::move(entry), loadCallback); - addAndMakeVisible(*card); - cards.push_back(std::move(card)); + 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(); @@ -113,17 +336,30 @@ class ModelRegistryList : public Component { auto area = getLocalBounds(); - for (auto& card : cards) - card->setBounds(area.removeFromTop(ModelRegistryCard::preferredHeight).reduced(0, 4)); + 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 { - return static_cast(cards.size()) * ModelRegistryCard::preferredHeight; + 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> cards; + std::vector> items; }; class HomeTab : public Component, @@ -157,6 +393,7 @@ class HomeTab : public Component, addAndMakeVisible(subtitleLabel); addAndMakeVisible(searchEditor); addAndMakeVisible(customPathButton); + addAndMakeVisible(categoryFilterBar); addAndMakeVisible(viewport); rebuildModelList(); @@ -180,6 +417,9 @@ class HomeTab : public Component, searchRow.removeFromRight(8); searchEditor.setBounds(searchRow); + area.removeFromTop(10); + categoryFilterBar.setBounds(area.removeFromTop(categoryFilterBar.getPreferredHeight())); + area.removeFromTop(10); viewport.setBounds(area); @@ -190,6 +430,7 @@ class HomeTab : public Component, { searchEditor.setEnabled(true); customPathButton.setEnabled(true); + categoryFilterBar.setEnabled(true); viewport.setEnabled(true); } @@ -211,6 +452,7 @@ class HomeTab : public Component, { searchEditor.setEnabled(false); customPathButton.setEnabled(false); + categoryFilterBar.setEnabled(false); viewport.setEnabled(false); if (onModelLoadRequested) @@ -235,10 +477,84 @@ class HomeTab : public Component, .toLowerCase(); if (searchText.isEmpty() || searchableText.contains(searchText)) - entries.push_back(std::move(entry)); + { + 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.setEntries(std::move(entries), + modelList.setSections(std::move(sections), [this](ModelRegistry::Entry entry) { requestModelLoad(entry); }); updateListBounds(); } @@ -276,6 +592,8 @@ class HomeTab : public Component, Label subtitleLabel; TextEditor searchEditor; TextButton customPathButton; + CategoryFilterBar categoryFilterBar { [this](String cat) { activeCategory = cat; rebuildModelList(); } }; + String activeCategory { "All" }; Viewport viewport; ModelRegistryList modelList; diff --git a/src/utils/ModelRegistry.h b/src/utils/ModelRegistry.h index 90f7e922..9e97f2bb 100644 --- a/src/utils/ModelRegistry.h +++ b/src/utils/ModelRegistry.h @@ -19,6 +19,12 @@ struct Entry 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 getFallbackModelDisplayName(const String& modelPath) @@ -38,59 +44,73 @@ inline std::vector getFeaturedModels() { "stability/text-to-audio", "Stable Audio Text to Audio", "Generate music, sound effects, or soundscapes from a text prompt.", - "Stability AI" }, + "Stability AI", + { "Generation" } }, { "stability/audio-to-audio", "Stable Audio Audio to Audio", "Create variations or transfer style using text and audio conditioning.", - "Stability AI" }, + "Stability AI", + { "Generation", "Effects" } }, { "teamup-tech/text2midi-symbolic-music-generation", "Text2Midi", "Generate symbolic MIDI music from a text description.", - "Hugging Face" }, + "Hugging Face", + { "Generation" } }, { "teamup-tech/demucs-source-separation", "Demucs", "Split a music recording into drums, bass, vocals, and instrumental stems.", - "Hugging Face" }, + "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" }, + "Hugging Face", + { "Analysis" } }, { "teamup-tech/transkun", "Transkun", "Transcribe musical audio into symbolic note events.", - "Hugging Face" }, + "Hugging Face", + { "Analysis" } }, { "teamup-tech/TRIA", "TRIA", "Generate drum accompaniment conditioned on rhythmic input.", - "Hugging Face" }, + "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" }, + "Hugging Face", + { "Generation" } }, { "teamup-tech/vampnet-conditional-music-generation", "VampNet", "Generate controllable variations of an input music recording.", - "Hugging Face" }, + "Hugging Face", + { "Generation", "Effects" } }, { "teamup-tech/harmonic-percussive-separation", "Harmonic/Percussive Separation", "Separate audio into harmonic and percussive components.", - "Hugging Face" }, + "Hugging Face", + { "Source Separation" } }, { "teamup-tech/Kokoro-TTS", "Kokoro TTS", "Generate speech from text using a selected voice preset.", - "Hugging Face" }, + "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" }, + "Hugging Face", + { "Performance Rendering and Synthesis" } }, { "teamup-tech/midi-synthesizer", "MIDI Synthesizer", "Render MIDI into audio using the standard MuseScore SoundFont.", - "Hugging Face" }, + "Hugging Face", + { "Performance Rendering and Synthesis" } }, { "teamup-tech/audioseal", "AudioSeal", "Apply or inspect audio watermarking for generated audio workflows.", - "Hugging Face" }, + "Hugging Face", + { "Analysis", "Production" } }, }; } From 3b2a7920ddd15cf7bfba69f9f45523d9c19eec54 Mon Sep 17 00:00:00 2001 From: 2cylu2 <2cylu2@gmail.com> Date: Tue, 4 Aug 2026 10:16:30 -0400 Subject: [PATCH 9/9] Fix model loading UI behavior and failed reload state --- src/HomeTab.h | 50 +++++--- src/ModelTab.h | 21 ++++ src/ModelTabContainer.h | 44 +++++-- src/utils/ModelRegistry.h | 18 ++- src/widgets/ModelSelectionWidget.h | 189 +++++++++++++++-------------- 5 files changed, 196 insertions(+), 126 deletions(-) diff --git a/src/HomeTab.h b/src/HomeTab.h index 8e4e554c..f70f921b 100644 --- a/src/HomeTab.h +++ b/src/HomeTab.h @@ -22,18 +22,21 @@ class TagLabel : public Component public: TagLabel(const String& text) : tagText(text) { - setSize(font.getStringWidth(tagText) + 12, 18); + 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(0xff2d2d35)); + g.setColour(Colour(0xff183238)); g.fillRoundedRectangle(bounds, 4.0f); - g.setColour(Colour(0xff4f46e5).withAlpha(0.4f)); + g.setColour(Colour(0xff2dd4bf).withAlpha(0.42f)); g.drawRoundedRectangle(bounds, 4.0f, 1.0f); - g.setColour(Colour(0xffa5b4fc)); + g.setColour(Colour(0xff9eeadf)); g.setFont(font); g.drawText(tagText, getLocalBounds(), Justification::centred, true); } @@ -41,6 +44,8 @@ class TagLabel : public Component private: String tagText; Font font { 10.0f, Font::bold }; + static constexpr int horizontalPadding = 7; + static constexpr int verticalPadding = 4; }; class CategoryChip : public Button @@ -62,6 +67,9 @@ class CategoryChip : public Button 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); @@ -71,12 +79,12 @@ class CategoryChip : public Button if (isSelected) { - bg = Colour(0xff4f46e5); + bg = Colour(0xff0f766e); textColour = Colours::white; } else if (shouldDrawButtonAsHighlighted || shouldDrawButtonAsDown) { - bg = Colour(0xff2d2d30); + bg = Colour(0xff263a3d); textColour = Colours::white; } else @@ -86,18 +94,21 @@ class CategoryChip : public Button } g.setColour(bg); - g.fillRoundedRectangle(bounds, bounds.getHeight() * 0.5f); + g.fillRoundedRectangle(bounds, 5.0f); - g.setColour(isSelected ? Colour(0xff818cf8) : Colours::white.withAlpha(0.1f)); - g.drawRoundedRectangle(bounds, bounds.getHeight() * 0.5f, 1.0f); + g.setColour(isSelected ? Colour(0xff5eead4) : Colours::white.withAlpha(0.1f)); + g.drawRoundedRectangle(bounds, 5.0f, 1.0f); g.setColour(textColour); - g.setFont(Font(13.0f, Font::bold)); - g.drawText(getName(), getLocalBounds(), Justification::centred, true); + 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 @@ -146,25 +157,28 @@ class CategoryFilterBar : public Component auto area = getLocalBounds(); int x = 0; int y = 0; - int rowHeight = 28; int spacingX = 6; int spacingY = 6; + int rowHeight = 0; for (auto& chip : chips) { - int chipWidth = Font(13.0f, Font::bold).getStringWidth(chip->getName()) + 24; + 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, rowHeight); + chip->setBounds(x, y, chipWidth, chipHeight); x += chipWidth + spacingX; + rowHeight = jmax(rowHeight, chipHeight); } - int newHeight = y + rowHeight; + int newHeight = y + jmax(rowHeight, 1); if (newHeight != preferredHeight) { preferredHeight = newHeight; @@ -199,7 +213,7 @@ class CategoryHeader : public Component g.drawText(categoryName, getLocalBounds().reduced(4, 0), Justification::centredLeft, true); auto textWidth = Font(16.0f, Font::bold).getStringWidth(categoryName); - g.setColour(Colour(0xff4f46e5).withAlpha(0.6f)); + g.setColour(Colour(0xff2dd4bf).withAlpha(0.6f)); g.fillRect(textWidth + 12.0f, bounds.getCentreY() - 1.0f, bounds.getWidth() - textWidth - 16.0f, 2.0f); } @@ -274,7 +288,9 @@ class ModelRegistryCard : public Component for (auto& tagLabel : tagLabels) { - tagLabel->setBounds(topRow.removeFromRight(tagLabel->getWidth() + 4).reduced(0, 1)); + tagLabel->setBounds(topRow.removeFromRight(tagLabel->getPreferredWidth() + 4) + .withSizeKeepingCentre(tagLabel->getPreferredWidth(), + tagLabel->getPreferredHeight())); } nameLabel.setBounds(area.removeFromTop(24)); diff --git a/src/ModelTab.h b/src/ModelTab.h index e790eff1..a8155bdf 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -6,6 +6,8 @@ #pragma once +#include + #include #include "Model.h" @@ -62,6 +64,11 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas modelSelectionWidget.loadModelBypass(modelPath); } + void onNextModelLoadComplete(std::function callback) + { + initialLoadCallback = std::move(callback); + } + // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const { @@ -437,6 +444,8 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas // Re-enable processing immediately processCancelButton.setEnabled(true); + + notifyInitialLoadComplete(true); } else { @@ -448,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); @@ -456,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; @@ -607,4 +627,5 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool processingThreadPool { 10 }; std::atomic currentProcessID { 0 }; + std::function initialLoadCallback; }; diff --git a/src/ModelTabContainer.h b/src/ModelTabContainer.h index 602fa44e..7c9320b7 100644 --- a/src/ModelTabContainer.h +++ b/src/ModelTabContainer.h @@ -126,7 +126,6 @@ class ModelTabContainer : public TabbedComponent, int index = getNumTabs(); auto* tab = new ModelTab(); - tab->addChangeListener(this); auto tabName = modelName; @@ -136,14 +135,7 @@ class ModelTabContainer : public TabbedComponent, if (tabName.isEmpty()) tabName = "Model " + String(index); - addTab(tabName, - tabBackgroundColour, - tab, - true); - - addCloseButtonToModelTab(tab); - - setCurrentTabIndex(getNumTabs() - 1); + addLoadedModelTab(tab, tabName); if (modelPath.isNotEmpty()) tab->loadModelPath(modelPath); @@ -168,6 +160,20 @@ class ModelTabContainer : public TabbedComponent, } 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"); @@ -210,8 +216,24 @@ class ModelTabContainer : public TabbedComponent, auto* homeTab = new HomeTab(); homeTab->onModelLoadRequested = [this, homeTab](String modelPath, String modelName) { - createNewTab(modelPath, modelName); - homeTab->resetSelection(); + 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", diff --git a/src/utils/ModelRegistry.h b/src/utils/ModelRegistry.h index 9e97f2bb..71e56503 100644 --- a/src/utils/ModelRegistry.h +++ b/src/utils/ModelRegistry.h @@ -27,9 +27,19 @@ struct Entry {} }; +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 = modelPath.upToFirstOccurrenceOf(" [", false, false).trim(); + auto cleaned = getCleanModelPath(modelPath).upToFirstOccurrenceOf(" [", false, false).trim(); auto tokens = StringArray::fromTokens(cleaned, "/", ""); if (tokens.size() > 0) @@ -126,19 +136,19 @@ inline std::vector getFeaturedModelPaths() inline Entry getEntryForPath(const String& modelPath) { - const auto cleanedPath = modelPath.upToFirstOccurrenceOf(" [", false, false).trim(); + const auto cleanedPath = getCleanModelPath(modelPath).upToFirstOccurrenceOf(" [", false, false).trim(); for (const auto& entry : getFeaturedModels()) { if (entry.path == cleanedPath) { auto result = entry; - result.path = modelPath; + result.path = cleanedPath; return result; } } - return { modelPath, + return { cleanedPath, getFallbackModelDisplayName(modelPath), "Custom or recently used HARP-compatible model endpoint.", cleanedPath.startsWith("stability/") ? "Stability AI" : "Custom" }; diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index 82bed1e1..b43c3086 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -8,6 +8,7 @@ #include #include +#include #include @@ -25,6 +26,14 @@ using namespace juce; struct SharedChoices : public ChangeBroadcaster { + enum class LoadStatus + { + None, + Error, + Down, + TryAgain + }; + SharedChoices() : savedModelPaths(ModelRegistry::getFeaturedModelPaths()) { @@ -33,10 +42,11 @@ struct SharedChoices : public ChangeBroadcaster 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; @@ -52,17 +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(); + } + + 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 @@ -201,7 +277,7 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public void loadModelBypass(const String& modelPath) { - selectedPath = modelPath; + selectedPath = SharedChoices::stripStatusTag(modelPath.toStdString()); sendChangeMessage(); } @@ -236,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); - - // 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); + // Add a new entry for custom path + sharedChoices->addNewPath(loadedPath); - // 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(); @@ -305,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; @@ -373,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); } } @@ -437,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(); } @@ -529,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;