diff --git a/.gitignore b/.gitignore index b41816c2..9722f889 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ libtorch/ testproject scratch _downloads +artifacts/ packaging/dmg packaging/*.dmg diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..a31c1439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,8 @@ target_sources(${PROJECT_NAME} src/Main.cpp src/Application.cpp src/MainComponent.cpp + src/HomeTab.h + src/ModelTabContainer.h src/ModelTab.h src/Model.h @@ -111,6 +113,7 @@ target_sources(${PROJECT_NAME} src/utils/Logging.h src/utils/Settings.h src/utils/Interface.h + src/utils/ModelRegistry.h src/utils/Controls.h src/utils/Labels.h src/utils/Clients.h diff --git a/src/HomeTab.h b/src/HomeTab.h new file mode 100644 index 00000000..cd1b2e90 --- /dev/null +++ b/src/HomeTab.h @@ -0,0 +1,283 @@ +/** + * @file HomeTab.h + * @brief Home tab for model discovery and loading. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "utils/Interface.h" +#include "utils/ModelRegistry.h" +#include "widgets/ModelSelectionWidget.h" + +using namespace juce; + +class 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() + { + sharedChoices->addChangeListener(this); + + titleLabel.setText("Models", dontSendNotification); + titleLabel.setJustificationType(Justification::centredLeft); + titleLabel.setFont(Font(24.0f, Font::bold)); + + subtitleLabel.setText("Search HARP-compatible models and open one in a new tab.", + dontSendNotification); + subtitleLabel.setJustificationType(Justification::centredLeft); + + searchEditor.setTextToShowWhenEmpty("Search models...", Colours::grey); + searchEditor.setMultiLine(false); + searchEditor.setReturnKeyStartsNewLine(false); + searchEditor.onTextChange = [this] { rebuildModelList(); }; + + customPathButton.setButtonText("Custom Path"); + customPathButton.onClick = [this] { openCustomPathPopup(); }; + + viewport.setViewedComponent(&modelList, false); + viewport.setScrollBarsShown(true, false); + + addAndMakeVisible(titleLabel); + addAndMakeVisible(subtitleLabel); + addAndMakeVisible(searchEditor); + addAndMakeVisible(customPathButton); + addAndMakeVisible(viewport); + + rebuildModelList(); + } + + ~HomeTab() override + { + sharedChoices->removeChangeListener(this); + } + + void resized() override + { + auto area = getLocalBounds().reduced(16); + + titleLabel.setBounds(area.removeFromTop(34)); + subtitleLabel.setBounds(area.removeFromTop(26)); + + area.removeFromTop(8); + auto searchRow = area.removeFromTop(34); + customPathButton.setBounds(searchRow.removeFromRight(120).reduced(0, 1)); + searchRow.removeFromRight(8); + searchEditor.setBounds(searchRow); + + area.removeFromTop(10); + viewport.setBounds(area); + + updateListBounds(); + } + + void resetSelection() + { + searchEditor.setEnabled(true); + customPathButton.setEnabled(true); + viewport.setEnabled(true); + } + + Rectangle getModelSelectBounds() const + { + return searchEditor.getBounds().expanded(2, 2); + } + + std::function onModelLoadRequested; + +private: + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (source == static_cast(sharedChoices)) + rebuildModelList(); + } + + void requestModelLoad(const ModelRegistry::Entry& entry) + { + searchEditor.setEnabled(false); + customPathButton.setEnabled(false); + viewport.setEnabled(false); + + if (onModelLoadRequested) + onModelLoadRequested(entry.path, entry.displayName); + } + + void rebuildModelList() + { + std::vector entries; + const auto searchText = searchEditor.getText().trim().toLowerCase(); + + for (const auto& savedPath : sharedChoices->savedModelPaths) + { + const String path(savedPath); + + if (path.startsWithIgnoreCase("click here")) + continue; + + auto entry = ModelRegistry::getEntryForPath(path); + const auto searchableText = + (entry.displayName + " " + entry.summary + " " + entry.path + " " + entry.provider) + .toLowerCase(); + + if (searchText.isEmpty() || searchableText.contains(searchText)) + 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; + TextEditor searchEditor; + TextButton customPathButton; + Viewport viewport; + ModelRegistryList modelList; + + SharedResourcePointer sharedChoices; +}; diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index a31a32df..d0d2600c 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -10,9 +10,9 @@ MainComponent::MainComponent() initializeMenuBar(); - mainModelTab.addChangeListener(this); + modelTabs.addChangeListener(this); - addAndMakeVisible(mainModelTab); + addAndMakeVisible(modelTabs); addAndMakeVisible(statusAreaWidget); addAndMakeVisible(mediaClipboardWidget); @@ -31,7 +31,7 @@ MainComponent::MainComponent() MainComponent::~MainComponent() { deinitializeMenuBar(); - mainModelTab.removeChangeListener(this); + modelTabs.removeChangeListener(this); } void MainComponent::paint(Graphics& g) @@ -83,6 +83,16 @@ void MainComponent::paintOverChildren(Graphics& g) } } +ModelTab* MainComponent::getCurrentModelTab() const +{ + return modelTabs.getCurrentModelTab(); +} + +ModelTab* MainComponent::getFirstModelTab() const +{ + return modelTabs.getFirstModelTab(); +} + void MainComponent::resized() { Rectangle fullArea = getLocalBounds(); @@ -92,13 +102,20 @@ void MainComponent::resized() fullArea.removeFromTop(LookAndFeel::getDefaultLookAndFeel().getDefaultMenuBarHeight())); #endif + + FlexBox fullWindow; fullWindow.flexDirection = FlexBox::Direction::row; FlexBox mainPanel; mainPanel.flexDirection = FlexBox::Direction::column; - mainPanel.items.add(FlexItem(mainModelTab).withFlex(1.0)); + mainPanel.items.add(FlexItem(modelTabs).withFlex(1.0)); + + auto bounds = getLocalBounds(); + + // Give full area to tabs + modelTabs.setBounds(bounds); if (showStatusArea) { @@ -128,8 +145,13 @@ void MainComponent::resized() } } + + void MainComponent::updateWindowConstraints() { + auto* tab = getCurrentModelTab(); + if (!tab) return; + if (auto* window = findParentComponentOfClass()) { // Compute percentage of total window width given to main panel @@ -138,12 +160,13 @@ void MainComponent::updateWindowConstraints() // Determine minimum width needed to display controls plus padding const int requiredMainPanelWidth = jmax(minimumMainPanelWidth, - mainModelTab.getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); - // Determine current width of main panel - const int mainPanelWidth = jmax(requiredMainPanelWidth, mainModelTab.getWidth()); - // Determine minimum height needed to display all model contents plus status widget + tab->getMinimumRequiredControlWidth() + minimumMainPanelHorPadding); + + const int mainPanelWidth = + jmax(requiredMainPanelWidth, tab->getWidth()); + const int requiredMainPanelHeight = - mainModelTab.getMinimumRequiredHeightForWidth(mainPanelWidth) + tab->getMinimumRequiredHeightForWidth(mainPanelWidth) + (showStatusArea ? statusAreaHeight : 0); // Determine effective minimum width of entire window @@ -407,61 +430,108 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::ensureTutorialModelLoaded() { - if (! mainModelTab.isModelLoaded()) - mainModelTab.loadDefaultModel(); + auto* tab = getCurrentModelTab(); + + if (tab == nullptr) + { + tab = modelTabs.createNewTab(); + modelTabs.setCurrentTabIndex(0); + + if (welcomeWindow != nullptr) + tab->addChangeListener(welcomeWindow.get()); + + if (tab != nullptr) + tab->loadDefaultModel(); + return; + } + + if (! tab->isModelLoaded()) + tab->loadDefaultModel(); } void MainComponent::resetTutorialAutoLoadedModel() { - if (! mainModelTab.isModelLoaded()) - return; - - if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) + if (auto* tab = getCurrentModelTab()) { - mainModelTab.resetState(); + if (tab->isModelLoaded() && tab->getLoadedPath() == TutorialConstants::fallbackModelPath) + tab->resetState(); } } Rectangle MainComponent::getModelSelectBounds() { - auto bounds = mainModelTab.getModelSelectBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* homeTab = dynamic_cast(modelTabs.getCurrentContentComponent())) + { + auto bounds = homeTab->getModelSelectBounds(); + return getLocalArea(homeTab, bounds); + } + + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getModelSelectBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getControlsBounds() { - auto bounds = mainModelTab.getControlsBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getControlsBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputTrackBounds() { - auto bounds = mainModelTab.getInputTrackBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputTrackBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputFolderBounds() { - auto bounds = mainModelTab.getInputFolderBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputFolderBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getInputPlayBounds() { - auto bounds = mainModelTab.getInputPlayBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getInputPlayBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getProcessButtonBounds() { - auto bounds = mainModelTab.getProcessButtonBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getProcessButtonBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getTracksBounds() { - auto bounds = mainModelTab.getTracksBounds(); - return getLocalArea(&mainModelTab, bounds); + if (auto* tab = getCurrentModelTab()) + { + auto bounds = tab->getTracksBounds(); + return getLocalArea(tab, bounds); + } + return {}; } Rectangle MainComponent::getClipboardBounds() @@ -607,7 +677,7 @@ void MainComponent::focusCallback() void MainComponent::changeListenerCallback(ChangeBroadcaster* source) { - if (source == &mainModelTab) + if (source == &modelTabs) { updateWindowConstraints(); } diff --git a/src/MainComponent.h b/src/MainComponent.h index b229d7a4..4d777918 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -9,6 +9,7 @@ #include #include "ModelTab.h" +#include "ModelTabContainer.h" #include "clients/Client.h" @@ -74,14 +75,16 @@ class MainComponent : public Component, /* Tutorial */ - ModelTab* getModelTab() { return &mainModelTab; } - void setTutorialActive(bool active); void setTutorialHighlight(Rectangle bounds); void setTutorialExtraHighlights(std::vector> bounds); void ensureTutorialModelLoaded(); void resetTutorialAutoLoadedModel(); + ModelTab* getCurrentModelTab() const; + ModelTab* getFirstModelTab() const; + + // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); Rectangle getControlsBounds(); @@ -126,7 +129,7 @@ class MainComponent : public Component, // Miscellaneous //void focusCallback(); - void changeListenerCallback(ChangeBroadcaster* source); + void changeListenerCallback(ChangeBroadcaster* source) override; /* Interface */ @@ -150,16 +153,17 @@ class MainComponent : public Component, bool showStatusArea; bool showMediaClipboard; - ModelTab mainModelTab; + + ModelTabContainer modelTabs; StatusAreaWidget statusAreaWidget; MediaClipboardWidget mediaClipboardWidget; - bool isTutorialActive = false; - Rectangle tutorialHighlightRect; - std::vector> tutorialExtraHighlights; - std::unique_ptr welcomeWindow; - - SharedResourcePointer sharedTokens; + bool isTutorialActive = false; + Rectangle tutorialHighlightRect; + std::vector> tutorialExtraHighlights; + std::unique_ptr welcomeWindow; + + SharedResourcePointer sharedTokens; SharedResourcePointer statusMessage; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) diff --git a/src/ModelTab.h b/src/ModelTab.h index 3c172ede..e790eff1 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -21,14 +21,13 @@ using namespace juce; -class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster +class ModelTab : public Component, private ChangeListener, public ChangeBroadcaster { public: ModelTab() { modelSelectionWidget.addChangeListener(this); - addAndMakeVisible(modelSelectionWidget); addAndMakeVisible(modelInfoWidget); addAndMakeVisible(controlAreaWidget); @@ -58,6 +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 new file mode 100644 index 00000000..602fa44e --- /dev/null +++ b/src/ModelTabContainer.h @@ -0,0 +1,238 @@ +/** + * @brief Adds tab container to HARP for MultiTabs + * @author JEYuhas + */ +#pragma once + +#include + +#include "HomeTab.h" +#include "Model.h" +#include "ModelTab.h" + +#include "widgets/ControlAreaWidget.h" +#include "widgets/ModelInfoWidget.h" +#include "widgets/ModelSelectionWidget.h" +#include "widgets/TrackAreaWidget.h" + +#include "utils/Errors.h" +#include "utils/Interface.h" +#include "utils/Logging.h" +#include "utils/ModelRegistry.h" +#include "utils/Tutorial.h" + +using namespace juce; + +class ModelTabsLookAndFeel : public LookAndFeel_V4 +{ +public: + void drawTabbedButtonBarBackground(TabbedButtonBar& bar, Graphics& g) override + { + g.fillAll(tabBarColour); + g.setColour(separatorColour); + g.fillRect(0, bar.getHeight() - 1, bar.getWidth(), 1); + } + + void drawTabAreaBehindFrontButton(TabbedButtonBar&, Graphics& g, int w, int h) override + { + g.setColour(separatorColour); + g.fillRect(0, h - 1, w, 1); + } + + void drawTabButton(TabBarButton& button, + Graphics& g, + bool isMouseOver, + bool isMouseDown) override + { + const auto isActive = button.isFrontTab(); + auto area = button.getActiveArea(); + + const auto fill = isActive + ? activeTabColour + : inactiveTabColour.brighter(isMouseOver || isMouseDown ? 0.08f : 0.0f); + + g.setColour(fill); + g.fillRect(area); + + if (button.getIndex() > 0) + { + g.setColour(separatorColour); + g.fillRect(area.getX(), area.getY() + 2, 1, area.getHeight() - 4); + } + + auto textArea = button.getTextArea().reduced(tabTextInset, 0); + + g.setColour(isActive ? activeTextColour + : inactiveTextColour); + + g.drawText(button.getButtonText(), + textArea, + Justification::centred, + true); + } + + int getTabButtonBestWidth(TabBarButton& button, int tabDepth) override + { + return button.getButtonText() == "Home" + ? homeTabWidth + : fixedTabWidth; + } + + void drawTabButtonText(TabBarButton&, + Graphics&, + bool /*isMouseOver*/, + bool /*isMouseDown*/) override + { + } + +private: + const Colour tabBarColour { Colour(0xff1f1f1f) }; + const Colour inactiveTabColour { Colour(0xff242424) }; + const Colour activeTabColour { Colour(0xff343434) }; + const Colour separatorColour { Colour(0xff4a4a4a) }; + const Colour activeTextColour { Colours::white }; + const Colour inactiveTextColour { Colour(0xffaeb0b4) }; + static constexpr int fixedTabWidth = 140; + static constexpr int homeTabWidth = 64; + static constexpr int tabTextInset = 10; +}; + +class ModelTabContainer : public TabbedComponent, + private ChangeListener, + public ChangeBroadcaster +{ +public: + ModelTabContainer() + : TabbedComponent(TabbedButtonBar::TabsAtTop) + { + getTabbedButtonBar().setLookAndFeel(&tabsLookAndFeel); + + setColour(TabbedComponent::backgroundColourId, tabBackgroundColour); + getTabbedButtonBar().setColour(TabbedButtonBar::tabTextColourId, Colours::white); + getTabbedButtonBar().setColour(TabbedButtonBar::frontTextColourId, Colours::white); + getTabbedButtonBar().setColour(TabbedButtonBar::tabOutlineColourId, tabBackgroundColour.darker(0.35f)); + getTabbedButtonBar().setColour(TabbedButtonBar::frontOutlineColourId, tabBackgroundColour.darker(0.35f)); + + createHomeTab(); + } + + ~ModelTabContainer() override + { + getTabbedButtonBar().setLookAndFeel(nullptr); + } + + ModelTab* createNewTab(const String& modelPath = {}, const String& modelName = {}) + { + int index = getNumTabs(); + + auto* tab = new ModelTab(); + tab->addChangeListener(this); + + 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); + + addCloseButtonToModelTab(tab); + + setCurrentTabIndex(getNumTabs() - 1); + + if (modelPath.isNotEmpty()) + tab->loadModelPath(modelPath); + + return tab; + } + + ModelTab* getCurrentModelTab() const + { + return dynamic_cast(getCurrentContentComponent()); + } + + ModelTab* getFirstModelTab() const + { + for (int i = 0; i < getNumTabs(); ++i) + { + if (auto* tab = dynamic_cast(getTabContentComponent(i))) + return tab; + } + + return nullptr; + } + +private: + void addCloseButtonToModelTab(ModelTab* tab) + { + auto* closeButton = new TextButton("x"); + closeButton->setTooltip("Close model tab"); + closeButton->setSize(18, 18); + closeButton->setColour(TextButton::buttonColourId, Colours::transparentBlack); + closeButton->setColour(TextButton::buttonOnColourId, Colours::transparentBlack); + closeButton->setColour(TextButton::textColourOffId, Colours::white); + closeButton->setColour(TextButton::textColourOnId, Colours::white); + closeButton->onClick = [this, tab] { closeModelTab(tab); }; + + if (auto* tabButton = getTabbedButtonBar().getTabButton(getNumTabs() - 1)) + tabButton->setExtraComponent(closeButton, TabBarButton::afterText); + } + + void closeModelTab(ModelTab* tabToClose) + { + for (int i = 1; i < getNumTabs(); ++i) + { + if (getTabContentComponent(i) == tabToClose) + { + const auto currentIndex = getCurrentTabIndex(); + const auto targetIndex = currentIndex == i ? jmax(0, i - 1) + : (currentIndex > i ? currentIndex - 1 + : currentIndex); + + removeTab(i); + + if (getNumTabs() > 0) + setCurrentTabIndex(jlimit(0, getNumTabs() - 1, targetIndex)); + + sendChangeMessage(); + return; + } + } + } + + void createHomeTab() + { + auto* homeTab = new HomeTab(); + homeTab->onModelLoadRequested = [this, homeTab](String modelPath, String modelName) + { + createNewTab(modelPath, modelName); + homeTab->resetSelection(); + }; + + addTab("Home", + tabBackgroundColour, + homeTab, + false); + + setCurrentTabIndex(0); + } + + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (dynamic_cast(source)) + { + sendChangeMessage(); // bubble up to MainComponent + } + } + + const Colour tabBackgroundColour { + getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground) + }; + + ModelTabsLookAndFeel tabsLookAndFeel; +}; diff --git a/src/clients/Client.h b/src/clients/Client.h index 5596eb01..7f134307 100644 --- a/src/clients/Client.h +++ b/src/clients/Client.h @@ -180,7 +180,7 @@ class Client { public: Client() = default; - virtual ~Client() {}; + virtual ~Client() = default; virtual String inferHostSlashModel(String modelPath) = 0; virtual String inferEndpointPath(String modelPath) = 0; @@ -271,7 +271,11 @@ class Client String& payloadJSON, std::vector& outputFiles, LabelList& labels) = 0; - virtual OpResult cancel(String modelPath) { return OpResult::ok(); } + virtual OpResult cancel(String modelPath) + { + ignoreUnused(modelPath); + return OpResult::ok(); + } const String emptyJSONBody = R"({"data": []})"; diff --git a/src/clients/GradioClient.h b/src/clients/GradioClient.h index 9b77390f..b1374fdf 100644 --- a/src/clients/GradioClient.h +++ b/src/clients/GradioClient.h @@ -227,7 +227,7 @@ class GradioClient : public Client return OpResult::ok(); } - OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) + OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) override { String responseJSON; @@ -334,7 +334,7 @@ class GradioClient : public Client OpResult process(String modelPath, String& payloadJSON, std::vector& outputFiles, - LabelList& labels) + LabelList& labels) override { String responseJSON; @@ -428,7 +428,7 @@ class GradioClient : public Client return OpResult::ok(); } - OpResult cancel(String modelPath) + OpResult cancel(String modelPath) override { String response; diff --git a/src/clients/providers/stability/StabilityClient.h b/src/clients/providers/stability/StabilityClient.h index 15f66c39..bba18d37 100644 --- a/src/clients/providers/stability/StabilityClient.h +++ b/src/clients/providers/stability/StabilityClient.h @@ -145,7 +145,7 @@ class StabilityClient : public Client return documentationPath; } - OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) + OpResult queryControls(String modelPath, DynamicObject::Ptr& controls) override { const char* jsonData; int jsonDataSize = 0; @@ -211,7 +211,7 @@ class StabilityClient : public Client OpResult process(String modelPath, String& payloadJSON, std::vector& outputFiles, - LabelList& labels) + LabelList& labels) override { DynamicObject::Ptr dataDict; diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..64c4b56c 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -191,7 +191,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to play playButtonInactiveInfo = MultiButton::Mode { "Play-Inactive", "Nothing to play.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontaudio::Play }; // Mode during playback stopButtonInfo = MultiButton::Mode { "Stop", @@ -213,7 +213,7 @@ void MediaDisplayComponent::initializeButtons() fontawesome::Folder }; chooseFileButtonInactiveInfo = MultiButton::Mode { "ChooseFile-Inactive", "Cannot choose file while processing.", - [this] {}, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Folder }; @@ -231,7 +231,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to save saveFileButtonInactiveInfo = MultiButton::Mode { "Save-Inactive", "Nothing to save.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Save }; saveFileButton.addMode(saveFileButtonActiveInfo); saveFileButton.addMode(saveFileButtonInactiveInfo); @@ -247,7 +247,7 @@ void MediaDisplayComponent::initializeButtons() // Mode when there is nothing to copy copyFileButtonInactiveInfo = MultiButton::Mode { "Copy-Inactive", "Nothing to copy.", - [this] {}, MultiButton::DrawingMode::IconOnly, + [] {}, MultiButton::DrawingMode::IconOnly, Colours::lightgrey, fontawesome::Copy }; copyFileButton.addMode(copyFileButtonActiveInfo); copyFileButton.addMode(copyFileButtonInactiveInfo); @@ -945,7 +945,7 @@ void MediaDisplayComponent::copyFileCallback() float MediaDisplayComponent::getPixelsPerSecond() { - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return getMediaWidth() / static_cast(visibleRange.getLength()); } @@ -957,7 +957,7 @@ float MediaDisplayComponent::getPixelsPerSecond() double MediaDisplayComponent::mediaXToTime(const float mX) { - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return static_cast(mX / getPixelsPerSecond()) + getTimeAtOrigin(); } @@ -971,7 +971,7 @@ float MediaDisplayComponent::timeToMediaX(const double t) { double t_ = jmin(getTotalLengthInSecs(), jmax(0.0, t)); - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { return static_cast(t_ - getTimeAtOrigin()) * getPixelsPerSecond(); } @@ -986,7 +986,7 @@ float MediaDisplayComponent::mediaXToDisplayX(const float mX) float offsetX = 0; float visibleStartX = 0; - if (visibleRange.getLength()) + if (visibleRange.getLength() > 0.0) { offsetX = static_cast(getTimeAtOrigin()) * getPixelsPerSecond(); visibleStartX = static_cast(visibleRange.getStart() * getPixelsPerSecond()); @@ -1347,7 +1347,7 @@ void MediaDisplayComponent::mouseUp(const MouseEvent& e) } } -void MediaDisplayComponent::mouseDoubleClick(const MouseEvent& e) +void MediaDisplayComponent::mouseDoubleClick(const MouseEvent& /*e*/) { // TODO - mouseUp/Down (selectTrack()) is still called before this @@ -1526,4 +1526,4 @@ void MediaDisplayComponent::clearLabels(int processingIdxCutoff) } resized(); // Remove overhead label panel -} \ No newline at end of file +} diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..7c118fdd 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -31,7 +31,7 @@ class ColorablePanel : public Component { public: ColorablePanel(Colour color = Colours::darkgrey) - : defaultColor(color), backgroundColor(color) {}; + : defaultColor(color), backgroundColor(color) {} void paint(Graphics& g) override { g.fillAll(backgroundColor); } @@ -312,4 +312,4 @@ class MediaDisplayComponent : public Component, SharedResourcePointer instructionsMessage; SharedResourcePointer statusMessage; -}; \ No newline at end of file +}; diff --git a/src/media/pianoroll/KeyboardComponent.hpp b/src/media/pianoroll/KeyboardComponent.hpp index 105ebea0..aeaa3e29 100644 --- a/src/media/pianoroll/KeyboardComponent.hpp +++ b/src/media/pianoroll/KeyboardComponent.hpp @@ -13,14 +13,14 @@ using namespace juce; class KeyboardComponent : public Component { public: - KeyboardComponent() {}; + KeyboardComponent() {} - ~KeyboardComponent() {}; + ~KeyboardComponent() override {} static const char* pitchNames[]; static const Array blackPitches; - void paint(Graphics& g); + void paint(Graphics& g) override; virtual bool isKeyboardComponent() { return true; } diff --git a/src/utils/Errors.h b/src/utils/Errors.h index fd9c8318..779fbc74 100644 --- a/src/utils/Errors.h +++ b/src/utils/Errors.h @@ -21,9 +21,9 @@ struct ClientError Type type; - String path; - String client; - String token; + String path {}; + String client {}; + String token {}; }; inline String toUserMessage(const ClientError& e) @@ -117,7 +117,7 @@ struct HttpError Request request; - String endpointPath; + String endpointPath {}; int statusCode = 0; }; @@ -226,7 +226,7 @@ struct GradioError Type type; - String endpointPath; + String endpointPath {}; }; inline String toUserMessage(const GradioError& e) @@ -266,8 +266,8 @@ struct JsonError Type type; - String stringJSON; - String key; + String stringJSON {}; + String key {}; }; inline String toUserMessage(const JsonError& e) @@ -354,7 +354,7 @@ struct ControlError Type type; - String controlType; + String controlType {}; }; inline String toUserMessage(const ControlError& e) diff --git a/src/utils/ModelRegistry.h b/src/utils/ModelRegistry.h new file mode 100644 index 00000000..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/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 4dd5d940..becb98e9 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -34,9 +34,9 @@ class MediaClipboardWidget : public Component, public ChangeListener addAndMakeVisible(trackArea); } - ~MediaClipboardWidget() { trackAreaWidget.removeChangeListener(this); } + ~MediaClipboardWidget() override { trackAreaWidget.removeChangeListener(this); } - void paint(Graphics& g) { g.fillAll(Colours::lightgrey.darker().withAlpha(0.5f)); } + void paint(Graphics& g) override { g.fillAll(Colours::lightgrey.darker().withAlpha(0.5f)); } void resized() override { diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index bba39217..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 @@ -480,7 +469,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/widgets/StatusAreaWidget.h b/src/widgets/StatusAreaWidget.h index 3c7189a3..27dc27d6 100644 --- a/src/widgets/StatusAreaWidget.h +++ b/src/widgets/StatusAreaWidget.h @@ -40,7 +40,7 @@ class MessageBox : public Component, ChangeListener public: MessageBox(float fontSize = 15.0f, Justification justification = Justification::centred) { - messageLabel.setFont(fontSize); + messageLabel.setFont(FontOptions { fontSize }); messageLabel.setColour(Label::textColourId, Colour(0xE0, 0xE0, 0xE0)); messageLabel.setJustificationType(justification); @@ -51,7 +51,7 @@ class MessageBox : public Component, ChangeListener ~MessageBox() override { sharedMessage->removeChangeListener(this); } - void paint(Graphics& g) + void paint(Graphics& g) override { g.setColour(Colour(0x33, 0x33, 0x33)); g.fillAll(); @@ -60,9 +60,9 @@ class MessageBox : public Component, ChangeListener g.drawRect(getLocalBounds(), 1); } - void resized() { messageLabel.setBounds(getLocalBounds()); } + void resized() override { messageLabel.setBounds(getLocalBounds()); } - void changeListenerCallback(ChangeBroadcaster* /*source*/) + void changeListenerCallback(ChangeBroadcaster* /*source*/) override { messageLabel.setText(sharedMessage->message, dontSendNotification); } @@ -84,7 +84,7 @@ class StatusAreaWidget : public Component addAndMakeVisible(statusBox); } - ~StatusAreaWidget() {} + ~StatusAreaWidget() override {} void resized() override { diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index ad3d7188..7450f6d5 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -54,7 +54,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - mainComponent->getModelTab()->addChangeListener(this); + if (auto* tab = mainComponent->getFirstModelTab()) + tab->addChangeListener(this); mainComponent->setTutorialActive(true); } @@ -66,7 +67,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent) { - mainComponent->getModelTab()->removeChangeListener(this); + if (auto* tab = mainComponent->getFirstModelTab()) + tab->removeChangeListener(this); mainComponent->setTutorialActive(false); } } @@ -78,7 +80,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; auto loadedPath = model ? model->getLoadedPath() : String(); autoLoadedByTutorialFallback = (loadedPath == TutorialConstants::fallbackModelPath); @@ -87,7 +90,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } else if (autoLoadedByTutorialFallback && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; auto loadedPath = model ? model->getLoadedPath() : String(); if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; @@ -134,7 +138,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (model && model->isLoaded()) { modelName = model->getMetadata().name; @@ -178,7 +183,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // 4. Configure Parameters (Dynamic) if (mainComponent) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (model && model->isLoaded()) { String controlsStepTitle = "Configure Parameters (Optional)"; @@ -714,7 +720,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (content->currentStep == 1 && mainComponent != nullptr) { - auto model = mainComponent->getModelTab()->getModel(); + auto* tab = mainComponent->getFirstModelTab(); + auto model = tab != nullptr ? tab->getModel() : nullptr; if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true;