From 622d3d0e9e152010f6cd1e2accccd8912d1a6175 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 12 Dec 2025 00:20:54 -0600 Subject: [PATCH 01/13] guided tutorial --- src/Main.cpp | 34 ++-- src/MainComponent.h | 102 ++++++++++- src/windows/WelcomeWindow.h | 356 ++++++++++++++++++++++++++---------- 3 files changed, 370 insertions(+), 122 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index b67f3588..88ffb47e 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -1,5 +1,5 @@ -#include "MainComponent.h" #include "AppSettings.h" +#include "MainComponent.h" #include "windows/WelcomeWindow.h" using namespace juce; @@ -67,7 +67,7 @@ class GuiAppApplication : public JUCEApplication bool showWelcome = true; - if (!forceShowWelcome) + if (! forceShowWelcome) { if (AppSettings::containsKey("showWelcomePopup")) showWelcome = AppSettings::getIntValue("showWelcomePopup", 1) == 1; @@ -75,19 +75,24 @@ class GuiAppApplication : public JUCEApplication if (showWelcome) { - MessageManager::callAsync([this]() - { - DialogWindow::LaunchOptions opts; - opts.dialogTitle = "Welcome"; - opts.content.setOwned(new WelcomeWindow([this]() + MessageManager::callAsync( + [this]() { - if (auto* mainComp = dynamic_cast(mainWindow->getContentComponent())) - mainComp->showSettingsDialog(); - })); - opts.content->setSize(480, 500); - opts.useNativeTitleBar = true; - opts.launchAsync(); - }); + auto* mainComp = + dynamic_cast(mainWindow->getContentComponent()); + if (mainComp) + { + welcomeWindow.reset(new WelcomeWindow(mainComp)); + + // Handle window closing + welcomeWindow->onClose = [this]() { welcomeWindow.reset(); }; + + // Position relative to main window or center + welcomeWindow->centreWithSize(500, 420); + welcomeWindow->setVisible(true); + welcomeWindow->toFront(true); + } + }); } StringArray args; @@ -526,6 +531,7 @@ class GuiAppApplication : public JUCEApplication } std::unique_ptr mainWindow; + std::unique_ptr welcomeWindow; Array> additionalWindows; ApplicationProperties applicationProperties; diff --git a/src/MainComponent.h b/src/MainComponent.h index 0a7cb0ce..a5db3982 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -12,11 +12,11 @@ #include #include +#include "ThreadPoolJob.h" +#include "WebModel.h" #include "widgets/ControlAreaWidget.h" #include "widgets/MediaClipboardWidget.h" -#include "ThreadPoolJob.h" #include "widgets/TrackAreaWidget.h" -#include "WebModel.h" #include "gui/CustomPathDialog.h" #include "gui/HoverHandler.h" @@ -535,18 +535,21 @@ class MainComponent : public Component, } else if (spaceInfo.status == SpaceInfo::Status::HUGGINGFACE) { - URL spaceUrl = this->model->getTempClient().getSpaceInfo().huggingface; + URL spaceUrl = + this->model->getTempClient().getSpaceInfo().huggingface; spaceUrl.launchInDefaultBrowser(); } else if (spaceInfo.status == SpaceInfo::Status::LOCALHOST) { // either choose hugingface or gradio, they are the same - URL spaceUrl = this->model->getTempClient().getSpaceInfo().huggingface; + URL spaceUrl = + this->model->getTempClient().getSpaceInfo().huggingface; spaceUrl.launchInDefaultBrowser(); } else if (spaceInfo.status == SpaceInfo::Status::STABILITY) { - URL spaceUrl = this->model->getTempClient().getSpaceInfo().stability; + URL spaceUrl = + this->model->getTempClient().getSpaceInfo().stability; spaceUrl.launchInDefaultBrowser(); } // URL spaceUrl = @@ -1215,8 +1218,7 @@ class MainComponent : public Component, [this, localInputTrackFiles](String jobProcessID) { // &jobsFinished, totalJobs // Individual job code for each iteration // copy the audio file, with the same filename except for an added _harp to the stem - OpResult processingResult = - model->process(localInputTrackFiles); + OpResult processingResult = model->process(localInputTrackFiles); processMutex.lock(); if (jobProcessID != currentProcessID) { @@ -1300,6 +1302,90 @@ class MainComponent : public Component, g.fillAll(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); } + void paintOverChildren(Graphics& g) override + { + if (! tutorialHighlightRect.isEmpty()) + { + auto area = getLocalBounds(); + + // Create a path for the background + Path backgroundPath; + backgroundPath.addRectangle(area.toFloat()); + + // Create a path for the highlight hole + // We use a rounded rectangle for a nicer look + Path highlightPath; + highlightPath.addRoundedRectangle(tutorialHighlightRect.toFloat(), 5.0f); + + // Subtract the highlight from the background + backgroundPath.setUsingNonZeroWinding(false); + backgroundPath.addPath(highlightPath); + + g.setColour(Colours::black.withAlpha(0.6f)); + g.fillPath(backgroundPath); + + // Optional: Draw a border around the highlight + g.setColour(Colours::white.withAlpha(0.8f)); + g.strokePath(highlightPath, PathStrokeType(2.0f)); + } + } + + void setTutorialHighlight(Rectangle bounds) + { + tutorialHighlightRect = bounds; + repaint(); + } + + // Bounds accessors for tutorial steps + Rectangle getModelSelectBounds() + { + // Combine Combobox and Load Button (Row 1) + if (modelPathComboBox.isVisible()) + { + auto bounds = modelPathComboBox.getBounds(); + bounds = bounds.getUnion(loadModelButton.getBounds()); + return bounds.expanded(5, 5); + } + return {}; + } + + Rectangle getControlsBounds() + { + if (controlAreaWidget.isVisible()) + return controlAreaWidget.getBounds().expanded(5, 5); + return {}; + } + + Rectangle getTracksBounds() + { + auto bounds = inputTrackAreaWidget.getBounds(); + if (outputTrackAreaWidget.isVisible()) + bounds = bounds.getUnion(outputTrackAreaWidget.getBounds()); + + // Include labels if visible + if (inputTracksLabel.isVisible()) + bounds = bounds.getUnion(inputTracksLabel.getBounds()); + if (outputTracksLabel.isVisible()) + bounds = bounds.getUnion(outputTracksLabel.getBounds()); + + return bounds.expanded(5, 5); + } + + Rectangle getInfoBarBounds() + { + auto bounds = instructionBox->getBounds(); + if (statusBox->isVisible()) + bounds = bounds.getUnion(statusBox->getBounds()); + return bounds.expanded(5, 5); + } + + Rectangle getClipboardBounds() + { + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + return mediaClipboardWidget.getBounds().expanded(5, 5); + return {}; + } + void resized() override { auto mainArea = getLocalBounds(); @@ -1751,4 +1837,6 @@ class MainComponent : public Component, } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) + + Rectangle tutorialHighlightRect; }; diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index c6fb9946..1881d0c7 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -1,128 +1,282 @@ #pragma once -#include "juce_gui_basics/juce_gui_basics.h" #include "../AppSettings.h" +#include "../MainComponent.h" +#include using namespace juce; -class WelcomeWindow : public Component +struct TutorialStep +{ + String title; + String description; + std::function(MainComponent*)> getHighlightBounds; +}; + +class WelcomeWindow : public DocumentWindow { public: - WelcomeWindow(std::function openSettingsCallback) - : onOpenSettings(std::move(openSettingsCallback)) + WelcomeWindow(MainComponent* mainComp) + : DocumentWindow("Welcome to HARP", + Desktop::getInstance().getDefaultLookAndFeel().findColour( + ResizableWindow::backgroundColourId), + DocumentWindow::allButtons), + mainComponent(mainComp) { - setSize(480, 500); - setColour(ResizableWindow::backgroundColourId, Colours::darkgrey); - - // --- Intro Text --- - introText.setText( - "Welcome to HARP!\n" - "A tool for hosted, asynchronous, remote processing of audio tracks.", - dontSendNotification); - introText.setJustificationType(Justification::centred); - introText.setFont(Font(17.0f, Font::bold)); - addAndMakeVisible(introText); - - // --- Instructions --- - instructions.setText( - "\n HARP operates as a standalone app or plugin-like editor within your DAW, allowing you " - "to process tracks using machine learning models hosted on platforms like Hugging Face or Stability AI.\n\n" - "The interface is organized into several sections:\n" - "1. The top area allows model selection and parameter control.\n" - "2. The middle area handles audio & MIDI I/O and processing.\n" - "3. The bottom shows model status & info for hovered component.\n" - "4. The rightmost panel can hold intermediate inputs / outputs and overwrite DAW-linked tracks.\n\n" - "Getting started:\n" - "Access tokens are required for models hosted on Hugging Face (as ZeroGPU spaces)" - " or Stability AI, to authenticate your account and let HARP securely fetch and run models. " - "The first two models in the dropdown are hosted by Stability AI, and the rest are hosted on Hugging Face.\n\n" - "Add tokens under File -> Settings -> Hugging Face, or by clicking 'Open Settings' below.", - dontSendNotification); - instructions.setJustificationType(Justification::centredTop); - instructions.setFont(Font(14.0f)); - addAndMakeVisible(instructions); - - // --- Buttons --- - openSettingsButton.setButtonText("Open Settings"); - openSettingsButton.onClick = [this]() - { - if (onOpenSettings) - onOpenSettings(); - }; - addAndMakeVisible(openSettingsButton); + setUsingNativeTitleBar(true); + setResizable(true, true); + setSize(500, 420); + setAlwaysOnTop(true); - // --- Checkbox --- - dontShowAgain.setButtonText("Don't show this again"); - dontShowAgain.setColour(ToggleButton::textColourId, Colours::white); - addAndMakeVisible(dontShowAgain); + // Define steps + steps = { + { "Welcome to HARP", + "HARP is your hub for hosted, asynchronous, remote audio processing.\n\n" + "HARP operates as a standalone app or plugin-like editor within your DAW, allowing you to process tracks using ML models hosted on platforms like Hugging Face or Stability AI.\n\n" + "This quick tutorial will guide you through the main features of the application.", + [](MainComponent*) { return Rectangle(); } }, - continueButton.setButtonText("Continue"); - continueButton.onClick = [this]() - { - const bool dontShow = dontShowAgain.getToggleState(); - AppSettings::setValue("showWelcomePopup", dontShow ? 0 : 1); - AppSettings::saveIfNeeded(); + { "Select a Model", + "Start by choosing a model from the dropdown list. We support HuggingSpace, Stability AI, and custom endpoints.\n\n" + "Click 'Load' to initialize the selected model.", + [](MainComponent* c) { return c->getModelSelectBounds(); } }, + + { "Configure Parameters", + "Adjust the model parameters here. These controls change dynamically based on the selected model.\n\n" + "You can hover over the components to view its description below in the status bar.", + [](MainComponent* c) { return c->getControlsBounds(); } }, + + { "Manage Tracks", + "Select or drag and drop your audio or MIDI files onto the input tracks.\n" + "Processed results will appear in the output tracks.", + [](MainComponent* c) { return c->getTracksBounds(); } }, + + { "Track Status", + "\nKeep an eye on the bottom bar for status updates, processing progress, and helpful instructions.", + [](MainComponent* c) { return c->getInfoBarBounds(); } }, + + { "Media Clipboard", + "Use the right-hand clipboard to manage your media files. You can drag processed files back to your DAW or reuse them as inputs.", + [](MainComponent* c) { return c->getClipboardBounds(); } }, - if (auto* window = findParentComponentOfClass()) - window->closeButtonPressed(); + { "All Set!", + "You're ready to start creating!\n\n" + "Don't forget to set up your API tokens in the Settings menu if you plan to use hosted models.\n" + "Add tokens under File -> Settings -> Hugging Face", + [](MainComponent*) { return Rectangle(); } } }; - addAndMakeVisible(continueButton); - - // --- View Documentation --- - docsLink.setButtonText("View Documentation"); - docsLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); - docsLink.setColour(HyperlinkButton::textColourId, Colours::skyblue); - addAndMakeVisible(docsLink); - - // --- Footer --- - footerLabel.setText("Copyright 2025 TEAMuP. All rights reserved.", - dontSendNotification); - footerLabel.setJustificationType(Justification::centred); - footerLabel.setFont(Font(13.0f)); - addAndMakeVisible(footerLabel); + + // UI Init + addAndMakeVisible(&titleLabel); + titleLabel.setFont(Font(24.0f, Font::bold)); + titleLabel.setJustificationType(Justification::centred); + + addAndMakeVisible(&descriptionLabel); + descriptionLabel.setFont(Font(16.0f)); + descriptionLabel.setJustificationType(Justification::centredTop); + + addAndMakeVisible(&learnMoreLink); + learnMoreLink.setButtonText("Learn more"); + learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); + learnMoreLink.setColour(HyperlinkButton::textColourId, Colours::skyblue); + + addAndMakeVisible(©rightLabel); + copyrightLabel.setText("Copyright 2025 TEAMuP. All rights reserved.", dontSendNotification); + copyrightLabel.setJustificationType(Justification::centred); + copyrightLabel.setFont(Font(12.0f)); + copyrightLabel.setColour(Label::textColourId, Colours::grey); + + addAndMakeVisible(&nextButton); + nextButton.setButtonText("Next"); + nextButton.onClick = [this] { nextStep(); }; + + addAndMakeVisible(&prevButton); + prevButton.setButtonText("Back"); + prevButton.onClick = [this] { prevStep(); }; + + addAndMakeVisible(&skipButton); + skipButton.setButtonText("Skip Tutorial"); + skipButton.onClick = [this] { skipTutorial(); }; + + addAndMakeVisible(&dontShowAgainToggle); + dontShowAgainToggle.setButtonText("Don't show this again"); + dontShowAgainToggle.setToggleState(false, dontSendNotification); + + addAndMakeVisible(&pageIndicator); + pageIndicator.setJustificationType(Justification::centred); + pageIndicator.setFont(Font(12.0f)); + + // Initial Update + updateStep(); + setVisible(true); + } + + void closeButtonPressed() override + { + // Reset highlight before closing + if (mainComponent) + mainComponent->setTutorialHighlight({}); + + if (onClose) + onClose(); } + std::function onClose; + void paint(Graphics& g) override { - g.fillAll(findColour(ResizableWindow::backgroundColourId)); + g.fillAll(getLookAndFeel().findColour(ResizableWindow::backgroundColourId)); } void resized() override { - // Get the content area dimensions - auto area = getLocalBounds(); - - // Define content width (same as original 480 width minus padding) - const int contentWidth = 440; - const int buttonWidth = 200; - - // Calculate horizontal center offset - const int centerX = (area.getWidth() - contentWidth) / 2; - const int buttonX = (area.getWidth() - buttonWidth) / 2; - - // Calculate vertical center offset for the entire content block - const int totalContentHeight = 485; // Approximate total height of all content - const int startY = jmax(0, (area.getHeight() - totalContentHeight) / 2); - - // Position all elements relative to center - introText.setBounds(centerX, startY + 15, contentWidth, 50); - instructions.setBounds(centerX + 10, startY + 70, contentWidth - 20, 270); - openSettingsButton.setBounds(buttonX, startY + 355, buttonWidth, 30); - dontShowAgain.setBounds(buttonX, startY + 390, buttonWidth, 24); - continueButton.setBounds(buttonX, startY + 420, buttonWidth, 30); - docsLink.setBounds(buttonX, startY + 455, buttonWidth, 20); - footerLabel.setBounds(centerX - 20, startY + 475, contentWidth + 40, 20); + auto area = getLocalBounds().reduced(20); + + // Header + titleLabel.setBounds(area.removeFromTop(40)); + area.removeFromTop(10); + + // Footer Buttons + auto footer = area.removeFromBottom(30); + auto buttonWidth = 100; + + skipButton.setBounds(footer.removeFromLeft(buttonWidth)); + + nextButton.setBounds(footer.removeFromRight(buttonWidth)); + footer.removeFromRight(10); + prevButton.setBounds(footer.removeFromRight(buttonWidth)); + + pageIndicator.setBounds(footer); + + // Spacer above footer + area.removeFromBottom(10); + + if (currentStep == 0) + { + // Step 1 Specific Layout + + // Copyright remains at the very bottom + copyrightLabel.setBounds(area.removeFromBottom(20)); + + // Layout from top down to keep Link close to Text + + // Description Area - explicit height to contain the text without massive gap + // 3 paragraphs of text at ~16px font + descriptionLabel.setBounds(area.removeFromTop(200)); + + // Link directly below description + auto linkArea = area.removeFromTop(30); + learnMoreLink.setBounds(linkArea.reduced(linkArea.getWidth() / 2 - 50, 0)); + + // Remaining area is empty space between link and copyright + } + else + { + // Standard Layout for other steps + + // Checkbox area (only visible on last page) + if (currentStep == (int) steps.size() - 1) + { + auto checkArea = area.removeFromBottom(30); + dontShowAgainToggle.setBounds(checkArea.removeFromRight(200)); + } + + // Description fills the rest + descriptionLabel.setBounds(area); + } } private: - std::function onOpenSettings; - Label introText; - Label instructions; - TextButton openSettingsButton; - ToggleButton dontShowAgain; - TextButton continueButton; - HyperlinkButton docsLink; - Label footerLabel; + void updateStep() + { + if (steps.empty()) + return; + + const auto& step = steps[size_t(currentStep)]; + + titleLabel.setText(step.title, dontSendNotification); + descriptionLabel.setText(step.description, dontSendNotification); + + // Highlight logic + if (mainComponent) + { + auto bounds = step.getHighlightBounds(mainComponent); + mainComponent->setTutorialHighlight(bounds); + } + + // Buttons + prevButton.setVisible(currentStep > 0); + + bool isLast = currentStep == (int) steps.size() - 1; + bool isFirst = currentStep == 0; + + nextButton.setButtonText(isLast ? "Finish" : "Next"); + skipButton.setVisible(! isLast); + dontShowAgainToggle.setVisible(isLast); + + // Items specific to first page + learnMoreLink.setVisible(isFirst); + copyrightLabel.setVisible(isFirst); + + pageIndicator.setText("Step " + String(currentStep + 1) + " of " + String(steps.size()), + dontSendNotification); + + // Trigger layout update since we change component visibility and position logic + resized(); + } + + void nextStep() + { + if (currentStep < (int) steps.size() - 1) + { + currentStep++; + updateStep(); + } + else + { + finishTutorial(); + } + } + + void prevStep() + { + if (currentStep > 0) + { + currentStep--; + updateStep(); + } + } + + void skipTutorial() + { + currentStep = (int) steps.size() - 1; + updateStep(); + } + + void finishTutorial() + { + if (dontShowAgainToggle.getToggleState()) + { + AppSettings::setValue("showWelcomePopup", 0); + AppSettings::saveIfNeeded(); + } + + closeButtonPressed(); + } + + MainComponent* mainComponent; + std::vector steps; + int currentStep = 0; + + Label titleLabel; + Label descriptionLabel; + TextButton nextButton; + TextButton prevButton; + TextButton skipButton; + ToggleButton dontShowAgainToggle; + Label pageIndicator; + HyperlinkButton learnMoreLink; + Label copyrightLabel; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WelcomeWindow) }; \ No newline at end of file From 4206e0b641194aee5402aa97c1d5f0161f949799 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Mon, 5 Jan 2026 20:40:20 -0600 Subject: [PATCH 02/13] Refine welcome popup tutorial flow --- src/MainComponent.h | 1652 +++++++++++++++-------------- src/WebModel.h | 2 +- src/media/MediaDisplayComponent.h | 14 + src/widgets/TrackAreaWidget.h | 24 + src/windows/WelcomeWindow.h | 475 +++++++-- 5 files changed, 1298 insertions(+), 869 deletions(-) diff --git a/src/MainComponent.h b/src/MainComponent.h index a5db3982..08860e93 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -118,6 +118,15 @@ class MainComponent : public Component, viewMediaClipboard = 0x3000 }; + ChangeBroadcaster& getLoadBroadcaster(); + std::shared_ptr getModel(); + + bool isModelLoaded() const; + + void loadDefaultModel(); + + // methods removed + StringArray getMenuBarNames() override { //DBG("getMenuBarNames() called"); @@ -132,43 +141,9 @@ class MainComponent : public Component, // In mac, we want the "about" command to be in the application menu ("HARP" tab) // For now, this is not used, as the extra commands appear grayed out - std::unique_ptr getMacExtraMenu() - { - auto menu = std::make_unique(); - menu->addCommandItem(&commandManager, CommandIDs::about); - menu->addCommandItem(&commandManager, CommandIDs::settings); - // menu->addCommandItem(&commandManager, CommandIDs::login); - return menu; - } - - PopupMenu getMenuForIndex([[maybe_unused]] int menuIndex, const String& menuName) override - { - PopupMenu menu; - - if (menuName == "File") - { - menu.addCommandItem(&commandManager, CommandIDs::open); - //menu.addCommandItem(&commandManager, CommandIDs::save); - menu.addCommandItem(&commandManager, CommandIDs::saveAs); - //menu.addCommandItem(&commandManager, CommandIDs::undo); - //menu.addCommandItem(&commandManager, CommandIDs::redo); - menu.addSeparator(); - menu.addCommandItem(&commandManager, CommandIDs::settings); - menu.addSeparator(); - // menu.addCommandItem(&commandManager, CommandIDs::login); - menu.addCommandItem(&commandManager, CommandIDs::about); - } - else if (menuName == "View") - { - menu.addCommandItem(&commandManager, CommandIDs::viewMediaClipboard); - } - else - { - DBG("Unknown menu name: " << menuName); - } + std::unique_ptr getMacExtraMenu(); - return menu; - } + PopupMenu getMenuForIndex([[maybe_unused]] int menuIndex, const String& menuName) override; void menuItemSelected(int menuItemID, int topLevelMenuIndex) override { @@ -179,100 +154,12 @@ class MainComponent : public Component, ApplicationCommandTarget* getNextCommandTarget() override { return nullptr; } // Fills the commands array with the commands that this component/target supports - void getAllCommands(Array& commands) override - { - const CommandID ids[] = { CommandIDs::open, CommandIDs::save, - CommandIDs::saveAs, CommandIDs::undo, - CommandIDs::redo, CommandIDs::about, - CommandIDs::settings, CommandIDs::viewMediaClipboard }; - commands.addArray(ids, numElementsInArray(ids)); - } + void getAllCommands(Array& commands) override; // Gets the information about a specific command - void getCommandInfo(CommandID commandID, ApplicationCommandInfo& result) override - { - switch (commandID) - { - case CommandIDs::open: - // The third argument here doesn't indicate the command position in the menu - // it rather serves as a tag to categorize the command - result.setInfo("Open...", "Opens a file", "File", 0); - result.addDefaultKeypress('o', ModifierKeys::commandModifier); - break; - case CommandIDs::save: - result.setInfo("Save", "Saves the current document", "File", 0); - result.addDefaultKeypress('s', ModifierKeys::commandModifier); - break; - case CommandIDs::saveAs: - result.setInfo( - "Save As...", "Saves the current document with a new name", "File", 0); - result.addDefaultKeypress( - 's', ModifierKeys::shiftModifier | ModifierKeys::commandModifier); - break; - case CommandIDs::undo: - result.setInfo("Undo", "Undoes the most recent operation", "File", 0); - result.addDefaultKeypress('z', ModifierKeys::commandModifier); - break; - case CommandIDs::redo: - result.setInfo("Redo", "Redoes the most recent operation", "File", 0); - result.addDefaultKeypress( - 'z', ModifierKeys::shiftModifier | ModifierKeys::commandModifier); - break; - case CommandIDs::about: - result.setInfo("About HARP", "Shows information about the application", "About", 0); - break; - case CommandIDs::viewMediaClipboard: - result.setInfo("Media Clipboard", "Toggles display of media clipboard", "View", 0); - result.setTicked(showMediaClipboard); - break; - case CommandIDs::settings: - result.setInfo("Settings", "Open the settings window", "Settings", 0); - break; - } - } + void getCommandInfo(CommandID commandID, ApplicationCommandInfo& result) override; - bool perform(const InvocationInfo& info) override - { - DBG("perform() called"); - switch (info.commandID) - { - case CommandIDs::open: - DBG("Open command invoked"); - openFileChooser(); - break; - /*case CommandIDs::save: - DBG("Save command invoked"); - saveCallback(); - break;*/ - case CommandIDs::saveAs: - DBG("Save As command invoked"); - mediaClipboardWidget.saveFileCallback(); - break; - case CommandIDs::undo: - DBG("Undo command invoked"); - undoCallback(); - break; - case CommandIDs::redo: - DBG("Redo command invoked"); - redoCallback(); - break; - case CommandIDs::about: - DBG("About command invoked"); - showAboutDialog(); - break; - case CommandIDs::viewMediaClipboard: - DBG("ViewMediaClipboard command invoked"); - viewMediaClipboardCallback(); - break; - case CommandIDs::settings: - DBG("Settings command invoked"); - showSettingsDialog(); - break; - default: - return false; - } - return true; - } + bool perform(const InvocationInfo& info) override; void showAboutDialog() { @@ -289,479 +176,19 @@ class MainComponent : public Component, dialog.launchAsync(); } - void undoCallback() - { - // DBG("Undoing last edit"); - - // // check if the audio file is loaded - // if (! mediaDisplay->isFileLoaded()) - // { - // // TODO - gray out undo option in this case? - // // Fail with beep, we should just ignore this if it doesn't make sense - // DBG("No file loaded to perform operation on"); - // juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - // return; - // } - - if (isProcessing) - { - DBG("Can't undo while processing occurring!"); - juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - return; - } - - // Iterate over all inputMediaDisplays and call the iteratePreviousTempFile() - auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); - - /*for (auto& inputMediaDisplay : inputMediaDisplays) - { - if (! inputMediaDisplay->iteratePreviousTempFile()) - { - DBG("Nothing to undo!"); - // juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - } - else - { - saveEnabled = true; - DBG("Undo callback completed successfully"); - } - }*/ - } - - void redoCallback() - { - // DBG("Redoing last edit"); - - // // check if the audio file is loaded - // if (! mediaDisplay->isFileLoaded()) - // { - // // TODO - gray out undo option in this case? - // // Fail with beep, we should just ignore this if it doesn't make sense - // DBG("No file loaded to perform operation on"); - // juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - // return; - // } - - if (isProcessing) - { - DBG("Can't redo while processing occurring!"); - juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - return; - } - - // Iterate over all inputMediaDisplays and call the iterateNextTempFile() - auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); - - /*for (auto& inputMediaDisplay : inputMediaDisplays) - { - if (! inputMediaDisplay->iterateNextTempFile()) - { - DBG("Nothing to redo!"); - // juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); - } - else - { - saveEnabled = true; - DBG("Redo callback completed successfully"); - } - }*/ - } - - void tryLoadSavedToken() - { - if (model == nullptr || model->getStatus() == ModelStatus::INITIALIZED) - return; - - auto& client = model->getClient(); - const auto spaceInfo = client.getSpaceInfo(); - - if (spaceInfo.status == SpaceInfo::Status::GRADIO - || spaceInfo.status == SpaceInfo::Status::HUGGINGFACE) - { - auto token = AppSettings::getString("huggingFaceToken", ""); - if (! token.isEmpty()) - { - client.setToken(token); - setStatus("Applied saved Hugging Face token."); - } - } - else if (spaceInfo.status == SpaceInfo::Status::STABILITY) - { - auto token = AppSettings::getString("stabilityToken", ""); - if (! token.isEmpty()) - { - client.setToken(token); - setStatus("Applied saved Stability token."); - } - } - } - - void loadModelCallback() - { - // Get the URL/path the user provided in the comboBox - std::string pathURL; - if (modelPathComboBox.getSelectedItemIndex() == 0) - pathURL = customPath; - else - pathURL = modelPathComboBox.getText().toStdString(); - - std::map params = { - { "url", pathURL }, - }; - // resetUI(); - - // disable the load button until the model is loaded - loadModelButton.setEnabled(false); - modelPathComboBox.setEnabled(false); - loadModelButton.setButtonText("loading..."); - - // disable the process button until the model is loaded - processCancelButton.setEnabled(false); - - // loading happens asynchronously. - threadPool.addJob( - [this, params] - { - try - { - juce::String loadingError; - - // set the last status to the current status - // If loading of the new model fails, - // we want to go back to the status we had before the failed attempt - model->setLastStatus(model->getStatus()); - - OpResult loadingResult = model->load(params); - if (loadingResult.failed()) - { - throw loadingResult.getError(); - } - - // loading succeeded - // Do some UI stuff to add the new model to the comboBox - // if it's not already there - // and update the lastSelectedItemIndex and lastLoadedModelItemIndex - MessageManager::callAsync( - [this, loadingResult] - { - resetUI(); - if (modelPathComboBox.getSelectedItemIndex() == 0) - { - bool alreadyInComboBox = false; - - for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) - { - if (modelPathComboBox.getItemText(i) - == (juce::String) customPath) - { - alreadyInComboBox = true; - modelPathComboBox.setSelectedId(i + 1); - lastSelectedItemIndex = i; - lastLoadedModelItemIndex = i; - } - } - - if (! alreadyInComboBox) - { - int new_id = modelPathComboBox.getNumItems() + 1; - modelPathComboBox.addItem(customPath, new_id); - modelPathComboBox.setSelectedId(new_id); - lastSelectedItemIndex = new_id - 1; - lastLoadedModelItemIndex = new_id - 1; - } - } - else - { - lastLoadedModelItemIndex = modelPathComboBox.getSelectedItemIndex(); - } - processLoadingResult(loadingResult); - }); - } - catch (Error& loadingError) - { - Error::fillUserMessage(loadingError); - LogAndDBG("Error in Model Loading:\n" + loadingError.devMessage); - auto msgOpts = - MessageBoxOptions() - .withTitle("Loading Error") - .withIconType(AlertWindow::WarningIcon) - .withTitle("Error") - .withMessage("An error occurred while loading the WebModel: \n" - + loadingError.userMessage); - // if (! String(e.what()).contains("404") - // && ! String(e.what()).contains("Invalid URL")) - if (loadingError.type != ErrorType::InvalidURL) - { - msgOpts = msgOpts.withButton("Open Space URL"); - } - - msgOpts = msgOpts.withButton("Open HARP Logs").withButton("Ok"); - auto alertCallback = [this, msgOpts, loadingError](int result) - { - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // NOTE (hugo): there's something weird about the button indices assigned by the msgOpts here - // DBG("ALERT-CALLBACK: buttonClicked alertCallback listener activated: chosen: " << chosen); - // auto chosen = msgOpts.getButtonText(result); - // they're not the same as the order of the buttons in the alert - // this is the order that I actually observed them to be. - // UPDATE/TODO (xribene): This should be fixed in Juce v8 - // see: https://forum.juce.com/t/wrong-callback-value-for-alertwindow-showokcancelbox/55671/2 - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - std::map observedButtonIndicesMap = {}; - if (msgOpts.getNumButtons() == 3) - { - observedButtonIndicesMap.insert( - { 1, "Open Space URL" }); // should actually be 0 right? - } - observedButtonIndicesMap.insert( - { msgOpts.getNumButtons() - 1, - "Open HARP Logs" }); // should actually be 1 - observedButtonIndicesMap.insert({ 0, "Ok" }); // should be 2 - - auto chosen = observedButtonIndicesMap[result]; - - if (chosen == "Open HARP Logs") - { - HarpLogger::getInstance()->getLogFile().revealToUser(); - } - else if (chosen == "Open Space URL") - { - // get the spaceInfo - SpaceInfo spaceInfo = model->getTempClient().getSpaceInfo(); - if (spaceInfo.status == SpaceInfo::Status::GRADIO) - { - URL spaceUrl = this->model->getTempClient().getSpaceInfo().gradio; - spaceUrl.launchInDefaultBrowser(); - } - else if (spaceInfo.status == SpaceInfo::Status::HUGGINGFACE) - { - URL spaceUrl = - this->model->getTempClient().getSpaceInfo().huggingface; - spaceUrl.launchInDefaultBrowser(); - } - else if (spaceInfo.status == SpaceInfo::Status::LOCALHOST) - { - // either choose hugingface or gradio, they are the same - URL spaceUrl = - this->model->getTempClient().getSpaceInfo().huggingface; - spaceUrl.launchInDefaultBrowser(); - } - else if (spaceInfo.status == SpaceInfo::Status::STABILITY) - { - URL spaceUrl = - this->model->getTempClient().getSpaceInfo().stability; - spaceUrl.launchInDefaultBrowser(); - } - // URL spaceUrl = - // this->model->getGradioClient().getSpaceInfo().huggingface; - // spaceUrl.launchInDefaultBrowser(); - } - - if (lastLoadedModelItemIndex == -1) - { - // If before the failed attempt to load a new model, we HAD NO model loaded - // TODO: these two functions we call here might be an overkill for this case - // we need to simplify - MessageManager::callAsync( - [this, loadingError] - { - resetModelPathComboBox(); - model->setStatus(ModelStatus::INITIALIZED); - processLoadingResult(OpResult::fail(loadingError)); - }); - } - else - { - // If before the failed attempt to load a new model, we HAD a model loaded - MessageManager::callAsync( - [this, loadingError] - { - // We set the status to - // the status of the model before the failed attempt - model->setStatus(model->getLastStatus()); - processLoadingResult(OpResult::fail(loadingError)); - }); - } - - // This if/elseif/else block is responsible for setting the selected item - // in the modelPathComboBox to the correct item (i.e the model/path/app that - // was selected before the failed attempt to load a new model) - // cb: sometimes setSelectedId it doesn't work and I dont know why. - // I've tried nesting it in MessageManage::callAsync, but still nothing. - if (lastLoadedModelItemIndex != -1) - { - modelPathComboBox.setSelectedId(lastLoadedModelItemIndex + 1); - } - else if (lastLoadedModelItemIndex == -1 && lastSelectedItemIndex != -1) - { - modelPathComboBox.setSelectedId(lastSelectedItemIndex + 1); - } - else - { - resetModelPathComboBox(); - MessageManager::callAsync([this, loadingError] - { loadModelButton.setEnabled(false); }); - } - /* - if (loadingError.userMessage.containsIgnoreCase("sleeping")) - { - MessageManager::callAsync( - [this] - { - addCustomPathToDropdown(customPath, true); // mark as sleeping - }); - } - //NEW: reopen custom path dialog if sleeping or 404 - if (loadingError.type == ErrorType::InvalidURL - || loadingError.devMessage.contains("404") - || loadingError.userMessage.containsIgnoreCase("sleeping")) - { - MessageManager::callAsync([this] { openCustomPathDialog(customPath); }); - } - */ - }; - - AlertWindow::showAsync(msgOpts, alertCallback); - saveEnabled = false; - } - catch (const std::exception& e) - { - // Catch any other standard exceptions (like std::runtime_error) - DBG("Caught std::exception: " << e.what()); - AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, - "Error", - "An unexpected error occurred: " - + juce::String(e.what())); - } - catch (...) // Catch any other exceptions - { - DBG("Caught unknown exception"); - AlertWindow::showMessageBoxAsync( - AlertWindow::WarningIcon, "Error", "An unexpected error occurred."); - } - }); - } - - void viewMediaClipboardCallback() - { - // Toggle media clipboard visibility state - showMediaClipboard = ! showMediaClipboard; - - // Find top-level window for resizing - if (auto* window = findParentComponentOfClass()) - { - // Determine which display contains HARP - auto* currentDisplay = - juce::Desktop::getInstance().getDisplays().getDisplayForRect(getScreenBounds()); - - int currentDisplayWidth; - - if (currentDisplay != nullptr) - { - if (window->isFullScreen()) - { - currentDisplayWidth = currentDisplay->totalArea.getWidth(); - } - else - { - currentDisplayWidth = currentDisplay->userArea.getWidth(); - } - } - - //int totalDesktopWidth = juce::Desktop::getInstance().getDisplays().getDisplayForRect(getBounds())->totalArea.getWidth(); - - // Get current bounds of top-level window - Rectangle windowBounds = window->getBounds(); - - if (showMediaClipboard) - { - // Scale bounds to extend window by 40% of main width - windowBounds.setWidth( - jmin(currentDisplayWidth, static_cast(1.4 * windowBounds.getWidth()))); - } - else - { - if (! window->isFullScreen()) - { - // Scale bounds to reduce window to main width - windowBounds.setWidth(static_cast(windowBounds.getWidth() / 1.4)); - } - } - - // Set extended or reduced bounds - window->setBounds(windowBounds); - } - - // Add view preference to persistent settings - AppSettings::setValue("showMediaClipboard", showMediaClipboard ? "1" : "0"); - AppSettings::saveIfNeeded(); - - // Send status message to add check to file menu - commandManager.commandStatusChanged(); - - resized(); - } - - void openCustomPathDialog(const std::string& prefillPath = "") - { - // Create and show the custom path dialog with a callback - std::function loadCallback = - [this](const juce::String& customPath2) - { - DBG("Custom path entered: " + customPath2); - this->customPath = customPath2.toStdString(); // Store the custom path - loadModelButton.triggerClick(); // Trigger the load model button click - }; - std::function cancelCallback = [this]() - { - // modelPathComboBox.setSelectedId(lastSelectedItemIndex); - if (lastLoadedModelItemIndex != -1) - { - modelPathComboBox.setSelectedId(lastLoadedModelItemIndex + 1); - } - else if (lastLoadedModelItemIndex == -1 && lastSelectedItemIndex != -1) - { - modelPathComboBox.setSelectedId(lastSelectedItemIndex + 1); - } - else - { - resetModelPathComboBox(); - MessageManager::callAsync([this] { loadModelButton.setEnabled(false); }); - } - }; + void undoCallback(); - CustomPathDialog* dialog = new CustomPathDialog(loadCallback, cancelCallback); - if (! prefillPath.empty()) - dialog->setTextFieldValue(prefillPath); - } + void redoCallback(); - void resetModelPathComboBox() - { - // cb: why do we resetUI inside a function named resetModelPathComboBox ? - resetUI(); - //should I clear this? - // spaceUrlButton.setButtonText(""); - // spaceUrlButtonHandler.detach(); + void tryLoadSavedToken(); - int numItems = modelPathComboBox.getNumItems(); - std::vector options; + void loadModelCallback(); - for (int i = 0; i < numItems; ++i) // item indexes are 1-based in JUCE - { - String itemText = modelPathComboBox.getItemText(i); - options.push_back(itemText.toStdString()); - DBG("Item index" << i << ": " << itemText); - } + void viewMediaClipboardCallback(); - modelPathComboBox.clear(); + void openCustomPathDialog(const std::string& prefillPath = ""); - modelPathComboBox.setTextWhenNothingSelected("choose a model"); - for (auto i = 0u; i < options.size(); ++i) - { - modelPathComboBox.addItem(options[i], static_cast(i) + 1); - } - lastSelectedItemIndex = -1; - } + void resetModelPathComboBox(); /* // Adds a path to the model dropdown if it's not already present @@ -999,6 +426,7 @@ class MainComponent : public Component, { modelPathComboBox.addItem(modelPaths[i], static_cast(i) + 1); } + modelPathComboBox.setSelectedId(1); modelPathComboBoxHandler.onMouseEnter = [this]() { setInstructions( @@ -1023,6 +451,21 @@ class MainComponent : public Component, }; addAndMakeVisible(modelPathComboBox); + + // Auto-load default model for first-time users + if (AppSettings::getIntValue("showWelcomePopup", 1) == 1) + { + // Find index for default model (demucs) + for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) + { + if (modelPathComboBox.getItemText(i) == "teamup-tech/demucs-source-separation") + { + modelPathComboBox.setSelectedId(i + 1); + loadModelButton.triggerClick(); + break; + } + } + } } // explicit MainComponent(const URL& initialFilePath = URL()) : jobsFinished(0), totalJobs(0) @@ -1126,176 +569,16 @@ class MainComponent : public Component, // commandManager.setFirstCommandTarget (nullptr); } - void cancelCallback() - { - DBG("HARPProcessorEditor::buttonClicked cancel button listener activated"); + void cancelCallback(); - OpResult cancelResult = model->cancel(); - - if (cancelResult.failed()) - { - LogAndDBG(cancelResult.getError().devMessage.toStdString()); - AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, - "Cancel Error", - "An error occurred while cancelling the processing: \n" - + cancelResult.getError().devMessage); - return; - } - // Update current process to empty - processMutex.lock(); - DBG("Cancel ProcessID: " + currentProcessID); - currentProcessID = ""; - processMutex.unlock(); - // We already added a temp file, so we need to undo that - // TODO: this is functionality that I need to add back // #TODO - // mediaDisplay->iteratePreviousTempFile(); - // mediaDisplay->clearFutureTempFiles(); - - // processCancelButton.setEnabled(false); // this is the og v3 - resetProcessingButtons(); // This is the new way - } - - void processCallback() - { - if (model == nullptr) - { - AlertWindow("Error", - "Model is not loaded. Please load a model first.", - AlertWindow::WarningIcon); - return; - } - - // Get new processID - String processID = juce::Uuid().toString(); - processMutex.lock(); - currentProcessID = processID; - DBG("Set Process ID: " + processID); - processMutex.unlock(); - - processCancelButton.setEnabled(true); - processCancelButton.setMode(cancelButtonInfo.label); - loadModelButton.setEnabled(false); - modelPathComboBox.setEnabled(false); - saveEnabled = false; - isProcessing = true; - - // mediaDisplay->addNewTempFile(); - auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); - - // Get all the getTempFilePaths from the inputMediaDisplays - // and store them in a map/dictionary with the track name as the key - std::vector> localInputTrackFiles; - for (auto& inputMediaDisplay : inputMediaDisplays) - { - if (! inputMediaDisplay->isFileLoaded() && inputMediaDisplay->isRequired()) - { - AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, - "Error", - "Input file is not loaded for track " - + inputMediaDisplay->getTrackName() - + ". Please load an input file first."); - processCancelButton.setMode(processButtonInfo.label); - isProcessing = false; - saveEnabled = true; - loadModelButton.setEnabled(true); - modelPathComboBox.setEnabled(true); - return; - } - if (inputMediaDisplay->isFileLoaded()) - { - //inputMediaDisplay->addNewTempFile(); - localInputTrackFiles.push_back( - std::make_tuple(inputMediaDisplay->getDisplayID(), - inputMediaDisplay->getTrackName(), - //inputMediaDisplay->getTempFilePath().getLocalFile())); - inputMediaDisplay->getOriginalFilePath().getLocalFile())); - } - } - - // Directly add the job to the thread pool - jobProcessorThread.addJob( - new CustomThreadPoolJob( - [this, localInputTrackFiles](String jobProcessID) { // &jobsFinished, totalJobs - // Individual job code for each iteration - // copy the audio file, with the same filename except for an added _harp to the stem - OpResult processingResult = model->process(localInputTrackFiles); - processMutex.lock(); - if (jobProcessID != currentProcessID) - { - DBG("ProcessID " + jobProcessID + " not found"); - DBG("NumJobs: " + std::to_string(jobProcessorThread.getNumJobs())); - DBG("NumThrds: " + std::to_string(jobProcessorThread.getNumThreads())); - processMutex.unlock(); - return; - } - if (processingResult.failed()) - { - Error processingError = processingResult.getError(); - Error::fillUserMessage(processingError); - LogAndDBG("Error in Processing:\n" - + processingError.devMessage.toStdString()); - AlertWindow::showMessageBoxAsync( - AlertWindow::WarningIcon, - "Processing Error", - "An error occurred while processing the audio file: \n" - + processingError.userMessage); - // cb: I commented this out, and it doesn't seem to change anything - // it was also causing a crash. If we need it, it needs to run on - // the message thread using MessageManager::callAsync - // hy: Now this line works. - // resetProcessingButtons(); - // cb: Needs to be in the message thread or else it crashes - // It's used when the processing fails to reset the process/cancel - // button back to the process mode. - MessageManager::callAsync([this] { resetProcessingButtons(); }); - processMutex.unlock(); - return; - } - // load the audio file again - DBG("ProcessID " + jobProcessID + " succeed"); - currentProcessID = ""; - model->setStatus(ModelStatus::FINISHED); - processBroadcaster.sendChangeMessage(); - processMutex.unlock(); - }, - processID), - true); - DBG("NumJobs: " + std::to_string(jobProcessorThread.getNumJobs())); - DBG("NumThrds: " + std::to_string(jobProcessorThread.getNumThreads())); - } + void processCallback(); /* Entry point for importing new files into the application. */ - void importNewFile(File mediaFile, bool fromDAW = false) - { - mediaClipboardWidget.addTrackFromFilePath(URL(mediaFile), fromDAW); + void importNewFile(File mediaFile, bool fromDAW = false); - if (! showMediaClipboard) - { - viewMediaClipboardCallback(); - } - } - - void openFileChooser() - { - StringArray validExtensions = MediaDisplayComponent::getSupportedExtensions(); - String filePatternsAllowed = "*" + validExtensions.joinIntoString(";*"); - - openFileBrowser = - std::make_unique("Select a media file...", File(), filePatternsAllowed); - - openFileBrowser->launchAsync(FileBrowserComponent::openMode - | FileBrowserComponent::canSelectFiles, - [this](const FileChooser& browser) - { - File chosenFile = browser.getResult(); - if (chosenFile != File {}) - { - importNewFile(chosenFile); - } - }); - } + void openFileChooser(); void paint(Graphics& g) override { @@ -1304,38 +587,66 @@ class MainComponent : public Component, void paintOverChildren(Graphics& g) override { - if (! tutorialHighlightRect.isEmpty()) + if (isTutorialActive) { auto area = getLocalBounds(); + g.setColour(Colours::black.withAlpha(0.6f)); - // Create a path for the background - Path backgroundPath; - backgroundPath.addRectangle(area.toFloat()); + if (tutorialHighlightRect.isEmpty() && tutorialExtraHighlights.empty()) + { + // Full dim if no highlight + g.fillAll(); + } + else + { + // Dim with cutout + Path backgroundPath; + backgroundPath.addRectangle(area.toFloat()); - // Create a path for the highlight hole - // We use a rounded rectangle for a nicer look - Path highlightPath; - highlightPath.addRoundedRectangle(tutorialHighlightRect.toFloat(), 5.0f); + Path highlightPath; + if (! tutorialHighlightRect.isEmpty()) + highlightPath.addRoundedRectangle(tutorialHighlightRect.toFloat(), 5.0f); - // Subtract the highlight from the background - backgroundPath.setUsingNonZeroWinding(false); - backgroundPath.addPath(highlightPath); + // Add extra highlights to the cutout path + for (auto& rect : tutorialExtraHighlights) + { + highlightPath.addRoundedRectangle(rect.toFloat(), 5.0f); + } - g.setColour(Colours::black.withAlpha(0.6f)); - g.fillPath(backgroundPath); + backgroundPath.setUsingNonZeroWinding(false); + backgroundPath.addPath(highlightPath); - // Optional: Draw a border around the highlight - g.setColour(Colours::white.withAlpha(0.8f)); - g.strokePath(highlightPath, PathStrokeType(2.0f)); + g.fillPath(backgroundPath); + + g.setColour(Colours::white); + g.drawRoundedRectangle(tutorialHighlightRect.toFloat(), 5.0f, 2.0f); + + for (auto& rect : tutorialExtraHighlights) + { + g.drawRoundedRectangle(rect.toFloat(), 5.0f, 2.0f); + } + } } } + void setTutorialActive(bool active) + { + isTutorialActive = active; + repaint(); + } + void setTutorialHighlight(Rectangle bounds) { tutorialHighlightRect = bounds; repaint(); } + void setTutorialExtraHighlights(std::vector> bounds) + { + tutorialExtraHighlights = bounds; + repaint(); + } + // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() { @@ -1344,47 +655,48 @@ class MainComponent : public Component, { auto bounds = modelPathComboBox.getBounds(); bounds = bounds.getUnion(loadModelButton.getBounds()); - return bounds.expanded(5, 5); + return bounds.expanded(2, 2); } return {}; } + Rectangle getLoadButtonBounds() + { + if (loadModelButton.isVisible()) + return loadModelButton.getBounds().expanded(5, 5); + return {}; + } + Rectangle getControlsBounds() { + if (controlAreaWidget.isVisible()) + return controlAreaWidget.getBounds().expanded(5, 5); if (controlAreaWidget.isVisible()) return controlAreaWidget.getBounds().expanded(5, 5); return {}; } - Rectangle getTracksBounds() + Rectangle getInputFolderBounds() { - auto bounds = inputTrackAreaWidget.getBounds(); - if (outputTrackAreaWidget.isVisible()) - bounds = bounds.getUnion(outputTrackAreaWidget.getBounds()); - - // Include labels if visible - if (inputTracksLabel.isVisible()) - bounds = bounds.getUnion(inputTracksLabel.getBounds()); - if (outputTracksLabel.isVisible()) - bounds = bounds.getUnion(outputTracksLabel.getBounds()); - - return bounds.expanded(5, 5); + auto bounds = inputTrackAreaWidget.getFirstTrackFolderButtonBounds(); + return getLocalArea(&inputTrackAreaWidget, bounds); } - Rectangle getInfoBarBounds() + Rectangle getInputPlayBounds() { - auto bounds = instructionBox->getBounds(); - if (statusBox->isVisible()) - bounds = bounds.getUnion(statusBox->getBounds()); - return bounds.expanded(5, 5); + auto bounds = inputTrackAreaWidget.getFirstTrackPlayButtonBounds(); + return getLocalArea(&inputTrackAreaWidget, bounds); } - Rectangle getClipboardBounds() - { - if (showMediaClipboard && mediaClipboardWidget.isVisible()) - return mediaClipboardWidget.getBounds().expanded(5, 5); - return {}; - } + Rectangle getInputTrackBounds() { return inputTrackAreaWidget.getBounds(); } + + Rectangle getProcessButtonBounds(); + + Rectangle getTracksBounds(); + + Rectangle getInfoBarBounds(); + + Rectangle getClipboardBounds(); void resized() override { @@ -1834,9 +1146,753 @@ class MainComponent : public Component, processCancelButton.grabKeyboardFocus(); resized(); repaint(); + + if (result.wasOk()) + loadBroadcaster.sendChangeMessage(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent) Rectangle tutorialHighlightRect; + std::vector> tutorialExtraHighlights; + bool isTutorialActive = false; }; + +// Implementations + +inline std::unique_ptr MainComponent::getMacExtraMenu() +{ + auto menu = std::make_unique(); + menu->addCommandItem(&commandManager, CommandIDs::about); + menu->addCommandItem(&commandManager, CommandIDs::settings); + // menu->addCommandItem(&commandManager, CommandIDs::login); + return menu; +} + +inline PopupMenu MainComponent::getMenuForIndex([[maybe_unused]] int menuIndex, + const String& menuName) +{ + PopupMenu menu; + + if (menuName == "File") + { + menu.addCommandItem(&commandManager, CommandIDs::open); + //menu.addCommandItem(&commandManager, CommandIDs::save); + menu.addCommandItem(&commandManager, CommandIDs::saveAs); + //menu.addCommandItem(&commandManager, CommandIDs::undo); + //menu.addCommandItem(&commandManager, CommandIDs::redo); + menu.addSeparator(); + menu.addCommandItem(&commandManager, CommandIDs::settings); + menu.addSeparator(); + // menu.addCommandItem(&commandManager, CommandIDs::login); + menu.addCommandItem(&commandManager, CommandIDs::about); + } + else if (menuName == "View") + { + menu.addCommandItem(&commandManager, CommandIDs::viewMediaClipboard); + } + else + { + DBG("Unknown menu name: " << menuName); + } + + return menu; +} + +inline ChangeBroadcaster& MainComponent::getLoadBroadcaster() { return loadBroadcaster; } + +inline std::shared_ptr MainComponent::getModel() { return model; } + +inline bool MainComponent::isModelLoaded() const { return model != nullptr && model->ready(); } + +inline void MainComponent::loadDefaultModel() +{ + // Check if a model is already loaded to avoid redundant reloads + if (isModelLoaded()) + return; + + // Find index for default model (demucs) + for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) + { + if (modelPathComboBox.getItemText(i) == "teamup-tech/demucs-source-separation") + { + modelPathComboBox.setSelectedId(i + 1); + loadModelButton + .triggerClick(); // Triggers the load asynchronously via the button handler + break; + } + } +} + +inline void MainComponent::getAllCommands(Array& commands) +{ + const CommandID ids[] = { CommandIDs::open, CommandIDs::save, + CommandIDs::saveAs, CommandIDs::undo, + CommandIDs::redo, CommandIDs::about, + CommandIDs::settings, CommandIDs::viewMediaClipboard }; + commands.addArray(ids, numElementsInArray(ids)); +} + +inline void MainComponent::getCommandInfo(CommandID commandID, ApplicationCommandInfo& result) +{ + switch (commandID) + { + case CommandIDs::open: + // The third argument here doesn't indicate the command position in the menu + // it rather serves as a tag to categorize the command + result.setInfo("Open...", "Opens a file", "File", 0); + result.addDefaultKeypress('o', ModifierKeys::commandModifier); + break; + case CommandIDs::save: + result.setInfo("Save", "Saves the current document", "File", 0); + result.addDefaultKeypress('s', ModifierKeys::commandModifier); + break; + case CommandIDs::saveAs: + result.setInfo("Save As...", "Saves the current document with a new name", "File", 0); + result.addDefaultKeypress('s', + ModifierKeys::shiftModifier | ModifierKeys::commandModifier); + break; + case CommandIDs::undo: + result.setInfo("Undo", "Undoes the most recent operation", "File", 0); + result.addDefaultKeypress('z', ModifierKeys::commandModifier); + break; + case CommandIDs::redo: + result.setInfo("Redo", "Redoes the most recent operation", "File", 0); + result.addDefaultKeypress('z', + ModifierKeys::shiftModifier | ModifierKeys::commandModifier); + break; + case CommandIDs::about: + result.setInfo("About HARP", "Shows information about the application", "About", 0); + break; + case CommandIDs::viewMediaClipboard: + result.setInfo("Media Clipboard", "Toggles display of media clipboard", "View", 0); + result.setTicked(showMediaClipboard); + break; + case CommandIDs::settings: + result.setInfo("Settings", "Open the settings window", "Settings", 0); + break; + } +} + +inline bool MainComponent::perform(const InvocationInfo& info) +{ + DBG("perform() called"); + switch (info.commandID) + { + case CommandIDs::open: + DBG("Open command invoked"); + openFileChooser(); + break; + /*case CommandIDs::save: + DBG("Save command invoked"); + saveCallback(); + break;*/ + case CommandIDs::saveAs: + DBG("Save As command invoked"); + mediaClipboardWidget.saveFileCallback(); + break; + case CommandIDs::undo: + DBG("Undo command invoked"); + undoCallback(); + break; + case CommandIDs::redo: + DBG("Redo command invoked"); + redoCallback(); + break; + case CommandIDs::about: + DBG("About command invoked"); + showAboutDialog(); + break; + case CommandIDs::viewMediaClipboard: + DBG("ViewMediaClipboard command invoked"); + viewMediaClipboardCallback(); + break; + case CommandIDs::settings: + DBG("Settings command invoked"); + showSettingsDialog(); + break; + default: + return false; + } + return true; +} + +inline void MainComponent::tryLoadSavedToken() +{ + if (model == nullptr || model->getStatus() == ModelStatus::INITIALIZED) + return; + + auto& client = model->getClient(); + const auto spaceInfo = client.getSpaceInfo(); + + if (spaceInfo.status == SpaceInfo::Status::GRADIO + || spaceInfo.status == SpaceInfo::Status::HUGGINGFACE) + { + auto token = AppSettings::getString("huggingFaceToken", ""); + if (! token.isEmpty()) + { + client.setToken(token); + setStatus("Applied saved Hugging Face token."); + } + } + else if (spaceInfo.status == SpaceInfo::Status::STABILITY) + { + auto token = AppSettings::getString("stabilityToken", ""); + if (! token.isEmpty()) + { + client.setToken(token); + setStatus("Applied saved Stability token."); + } + } +} + +inline void MainComponent::loadModelCallback() +{ + // Get the URL/path the user provided in the comboBox + std::string pathURL; + if (modelPathComboBox.getSelectedItemIndex() == 0) + pathURL = customPath; + else + pathURL = modelPathComboBox.getText().toStdString(); + + std::map params = { + { "url", pathURL }, + }; + // resetUI(); + + // disable the load button until the model is loaded + loadModelButton.setEnabled(false); + modelPathComboBox.setEnabled(false); + loadModelButton.setButtonText("loading..."); + + // disable the process button until the model is loaded + processCancelButton.setEnabled(false); + + // loading happens asynchronously. + threadPool.addJob( + [this, params] + { + try + { + juce::String loadingError; + + // set the last status to the current status + // If loading of the new model fails, + // we want to go back to the status we had before the failed attempt + model->setLastStatus(model->getStatus()); + + OpResult loadingResult = model->load(params); + if (loadingResult.failed()) + { + throw loadingResult.getError(); + } + + // loading succeeded + // Do some UI stuff to add the new model to the comboBox + // if it's not already there + // and update the lastSelectedItemIndex and lastLoadedModelItemIndex + MessageManager::callAsync( + [this, loadingResult] + { + resetUI(); + if (modelPathComboBox.getSelectedItemIndex() == 0) + { + bool alreadyInComboBox = false; + + for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) + { + if (modelPathComboBox.getItemText(i) == (juce::String) customPath) + { + alreadyInComboBox = true; + modelPathComboBox.setSelectedId(i + 1); + lastSelectedItemIndex = i; + lastLoadedModelItemIndex = i; + } + } + + if (! alreadyInComboBox) + { + int new_id = modelPathComboBox.getNumItems() + 1; + modelPathComboBox.addItem(customPath, new_id); + modelPathComboBox.setSelectedId(new_id); + lastSelectedItemIndex = new_id - 1; + lastLoadedModelItemIndex = new_id - 1; + } + } + else + { + lastLoadedModelItemIndex = modelPathComboBox.getSelectedItemIndex(); + } + processLoadingResult(loadingResult); + }); + } + catch (Error& loadingError) + { + Error::fillUserMessage(loadingError); + LogAndDBG("Error in Model Loading:\n" + loadingError.devMessage); + auto msgOpts = MessageBoxOptions() + .withTitle("Loading Error") + .withIconType(AlertWindow::WarningIcon) + .withTitle("Error") + .withMessage("An error occurred while loading the WebModel: \n" + + loadingError.userMessage); + // if (! String(e.what()).contains("404") + // && ! String(e.what()).contains("Invalid URL")) + if (loadingError.type != ErrorType::InvalidURL) + { + msgOpts = msgOpts.withButton("Open Space URL"); + } + + msgOpts = msgOpts.withButton("Open HARP Logs").withButton("Ok"); + auto alertCallback = [this, msgOpts, loadingError](int result) + { + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // NOTE (hugo): there's something weird about the button indices assigned by the msgOpts here + // DBG("ALERT-CALLBACK: buttonClicked alertCallback listener activated: chosen: " << chosen); + // auto chosen = msgOpts.getButtonText(result); + // they're not the same as the order of the buttons in the alert + // this is the order that I actually observed them to be. + // UPDATE/TODO (xribene): This should be fixed in Juce v8 + // see: https://forum.juce.com/t/wrong-callback-value-for-alertwindow-showokcancelbox/55671/2 + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + std::map observedButtonIndicesMap = {}; + if (msgOpts.getNumButtons() == 3) + { + observedButtonIndicesMap.insert( + { 1, "Open Space URL" }); // should actually be 0 right? + } + observedButtonIndicesMap.insert( + { msgOpts.getNumButtons() - 1, "Open HARP Logs" }); // should actually be 1 + observedButtonIndicesMap.insert({ 0, "Ok" }); // should be 2 + + auto chosen = observedButtonIndicesMap[result]; + + if (chosen == "Open HARP Logs") + { + HarpLogger::getInstance()->getLogFile().revealToUser(); + } + else if (chosen == "Open Space URL") + { + // get the spaceInfo + SpaceInfo spaceInfo = model->getTempClient().getSpaceInfo(); + if (spaceInfo.status == SpaceInfo::Status::GRADIO) + { + URL spaceUrl = this->model->getTempClient().getSpaceInfo().gradio; + spaceUrl.launchInDefaultBrowser(); + } + else if (spaceInfo.status == SpaceInfo::Status::HUGGINGFACE) + { + URL spaceUrl = this->model->getTempClient().getSpaceInfo().huggingface; + spaceUrl.launchInDefaultBrowser(); + } + else if (spaceInfo.status == SpaceInfo::Status::LOCALHOST) + { + // either choose hugingface or gradio, they are the same + URL spaceUrl = this->model->getTempClient().getSpaceInfo().huggingface; + spaceUrl.launchInDefaultBrowser(); + } + else if (spaceInfo.status == SpaceInfo::Status::STABILITY) + { + URL spaceUrl = this->model->getTempClient().getSpaceInfo().stability; + spaceUrl.launchInDefaultBrowser(); + } + // URL spaceUrl = + // this->model->getGradioClient().getSpaceInfo().huggingface; + // spaceUrl.launchInDefaultBrowser(); + } + + if (lastLoadedModelItemIndex == -1) + { + // If before the failed attempt to load a new model, we HAD NO model loaded + // TODO: these two functions we call here might be an overkill for this case + // we need to simplify + MessageManager::callAsync( + [this, loadingError] + { + resetModelPathComboBox(); + model->setStatus(ModelStatus::INITIALIZED); + processLoadingResult(OpResult::fail(loadingError)); + }); + } + else + { + // If before the failed attempt to load a new model, we HAD a model loaded + MessageManager::callAsync( + [this, loadingError] + { + // We set the status to + // the status of the model before the failed attempt + model->setStatus(model->getLastStatus()); + processLoadingResult(OpResult::fail(loadingError)); + }); + } + + // This if/elseif/else block is responsible for setting the selected item + // in the modelPathComboBox to the correct item (i.e the model/path/app that + // was selected before the failed attempt to load a new model) + // cb: sometimes setSelectedId it doesn't work and I dont know why. + // I've tried nesting it in MessageManage::callAsync, but still nothing. + if (lastLoadedModelItemIndex != -1) + { + modelPathComboBox.setSelectedId(lastLoadedModelItemIndex + 1); + } + else if (lastLoadedModelItemIndex == -1 && lastSelectedItemIndex != -1) + { + modelPathComboBox.setSelectedId(lastSelectedItemIndex + 1); + } + else + { + resetModelPathComboBox(); + MessageManager::callAsync([this, loadingError] + { loadModelButton.setEnabled(false); }); + } + }; + + AlertWindow::showAsync(msgOpts, alertCallback); + saveEnabled = false; + } + catch (const std::exception& e) + { + // Catch any other standard exceptions (like std::runtime_error) + DBG("Caught std::exception: " << e.what()); + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "Error", + "An unexpected error occurred: " + + juce::String(e.what())); + } + catch (...) // Catch any other exceptions + { + DBG("Caught unknown exception"); + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, "Error", "An unexpected error occurred."); + } + }); +} + +inline void MainComponent::viewMediaClipboardCallback() +{ + // Toggle media clipboard visibility state + showMediaClipboard = ! showMediaClipboard; + + // Find top-level window for resizing + if (auto* window = findParentComponentOfClass()) + { + // Determine which display contains HARP + auto* currentDisplay = + juce::Desktop::getInstance().getDisplays().getDisplayForRect(getScreenBounds()); + + int currentDisplayWidth; + + if (currentDisplay != nullptr) + { + if (window->isFullScreen()) + { + currentDisplayWidth = currentDisplay->totalArea.getWidth(); + } + else + { + currentDisplayWidth = currentDisplay->userArea.getWidth(); + } + } + + //int totalDesktopWidth = juce::Desktop::getInstance().getDisplays().getDisplayForRect(getBounds())->totalArea.getWidth(); + + // Get current bounds of top-level window + Rectangle windowBounds = window->getBounds(); + + if (showMediaClipboard) + { + // Scale bounds to extend window by 40% of main width + windowBounds.setWidth( + jmin(currentDisplayWidth, static_cast(1.4 * windowBounds.getWidth()))); + } + else + { + if (! window->isFullScreen()) + { + // Scale bounds to reduce window to main width + windowBounds.setWidth(static_cast(windowBounds.getWidth() / 1.4)); + } + } + + // Set extended or reduced bounds + window->setBounds(windowBounds); + } + + // Add view preference to persistent settings + AppSettings::setValue("showMediaClipboard", showMediaClipboard ? "1" : "0"); + AppSettings::saveIfNeeded(); + + // Send status message to add check to file menu + commandManager.commandStatusChanged(); + + resized(); +} + +inline void MainComponent::undoCallback() +{ + if (isProcessing) + { + DBG("Can't undo while processing occurring!"); + juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); + return; + } + // Iterate over all inputMediaDisplays and call the iteratePreviousTempFile() + auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); +} + +inline void MainComponent::redoCallback() +{ + if (isProcessing) + { + DBG("Can't redo while processing occurring!"); + juce::LookAndFeel::getDefaultLookAndFeel().playAlertSound(); + return; + } + // Iterate over all inputMediaDisplays and call the iterateNextTempFile() + auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); +} + +inline void MainComponent::openCustomPathDialog(const std::string& prefillPath) +{ + // Create and show the custom path dialog with a callback + std::function loadCallback = [this](const juce::String& customPath2) + { + DBG("Custom path entered: " + customPath2); + this->customPath = customPath2.toStdString(); // Store the custom path + loadModelButton.triggerClick(); // Trigger the load model button click + }; + std::function cancelCallback = [this]() + { + // modelPathComboBox.setSelectedId(lastSelectedItemIndex); + if (lastLoadedModelItemIndex != -1) + { + modelPathComboBox.setSelectedId(lastLoadedModelItemIndex + 1); + } + else if (lastLoadedModelItemIndex == -1 && lastSelectedItemIndex != -1) + { + modelPathComboBox.setSelectedId(lastSelectedItemIndex + 1); + } + else + { + resetModelPathComboBox(); + MessageManager::callAsync([this] { loadModelButton.setEnabled(false); }); + } + }; + + CustomPathDialog* dialog = new CustomPathDialog(loadCallback, cancelCallback); + if (! prefillPath.empty()) + dialog->setTextFieldValue(prefillPath); +} + +inline void MainComponent::resetModelPathComboBox() +{ + resetUI(); + + int numItems = modelPathComboBox.getNumItems(); + std::vector options; + + for (int i = 0; i < numItems; ++i) // item indexes are 1-based in JUCE + { + String itemText = modelPathComboBox.getItemText(i); + options.push_back(itemText.toStdString()); + DBG("Item index" << i << ": " << itemText); + } + + modelPathComboBox.clear(); + + modelPathComboBox.setTextWhenNothingSelected("choose a model"); + for (auto i = 0u; i < options.size(); ++i) + { + modelPathComboBox.addItem(options[i], static_cast(i) + 1); + } + lastSelectedItemIndex = -1; +} + +inline void MainComponent::cancelCallback() +{ + DBG("HARPProcessorEditor::buttonClicked cancel button listener activated"); + + OpResult cancelResult = model->cancel(); + + if (cancelResult.failed()) + { + LogAndDBG(cancelResult.getError().devMessage.toStdString()); + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "Cancel Error", + "An error occurred while cancelling the processing: \n" + + cancelResult.getError().devMessage); + return; + } + // Update current process to empty + processMutex.lock(); + DBG("Cancel ProcessID: " + currentProcessID); + currentProcessID = ""; + processMutex.unlock(); + + resetProcessingButtons(); // This is the new way +} + +inline void MainComponent::processCallback() +{ + if (model == nullptr) + { + AlertWindow( + "Error", "Model is not loaded. Please load a model first.", AlertWindow::WarningIcon); + return; + } + + // Get new processID + String processID = juce::Uuid().toString(); + processMutex.lock(); + currentProcessID = processID; + DBG("Set Process ID: " + processID); + processMutex.unlock(); + + processCancelButton.setEnabled(true); + processCancelButton.setMode(cancelButtonInfo.label); + loadModelButton.setEnabled(false); + modelPathComboBox.setEnabled(false); + saveEnabled = false; + isProcessing = true; + + // mediaDisplay->addNewTempFile(); + auto& inputMediaDisplays = inputTrackAreaWidget.getMediaDisplays(); + + // Get all the getTempFilePaths from the inputMediaDisplays + // and store them in a map/dictionary with the track name as the key + std::vector> localInputTrackFiles; + for (auto& inputMediaDisplay : inputMediaDisplays) + { + if (! inputMediaDisplay->isFileLoaded() && inputMediaDisplay->isRequired()) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "Error", + "Input file is not loaded for track " + + inputMediaDisplay->getTrackName() + + ". Please load an input file first."); + processCancelButton.setMode(processButtonInfo.label); + isProcessing = false; + saveEnabled = true; + loadModelButton.setEnabled(true); + modelPathComboBox.setEnabled(true); + return; + } + if (inputMediaDisplay->isFileLoaded()) + { + //inputMediaDisplay->addNewTempFile(); + localInputTrackFiles.push_back( + std::make_tuple(inputMediaDisplay->getDisplayID(), + inputMediaDisplay->getTrackName(), + //inputMediaDisplay->getTempFilePath().getLocalFile())); + inputMediaDisplay->getOriginalFilePath().getLocalFile())); + } + } + + // Directly add the job to the thread pool + jobProcessorThread.addJob( + new CustomThreadPoolJob( + [this, localInputTrackFiles](String jobProcessID) { // &jobsFinished, totalJobs + // Individual job code for each iteration + // copy the audio file, with the same filename except for an added _harp to the stem + OpResult processingResult = model->process(localInputTrackFiles); + processMutex.lock(); + if (jobProcessID != currentProcessID) + { + DBG("ProcessID " + jobProcessID + " not found"); + DBG("NumJobs: " + std::to_string(jobProcessorThread.getNumJobs())); + DBG("NumThrds: " + std::to_string(jobProcessorThread.getNumThreads())); + processMutex.unlock(); + return; + } + if (processingResult.failed()) + { + Error processingError = processingResult.getError(); + Error::fillUserMessage(processingError); + LogAndDBG("Error in Processing:\n" + processingError.devMessage.toStdString()); + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, + "Processing Error", + "An error occurred while processing the audio file: \n" + + processingError.userMessage); + MessageManager::callAsync([this] { resetProcessingButtons(); }); + processMutex.unlock(); + return; + } + // load the audio file again + DBG("ProcessID " + jobProcessID + " succeed"); + currentProcessID = ""; + model->setStatus(ModelStatus::FINISHED); + processBroadcaster.sendChangeMessage(); + processMutex.unlock(); + }, + processID), + true); + DBG("NumJobs: " + std::to_string(jobProcessorThread.getNumJobs())); + DBG("NumThrds: " + std::to_string(jobProcessorThread.getNumThreads())); +} + +inline void MainComponent::importNewFile(File mediaFile, bool fromDAW) +{ + mediaClipboardWidget.addTrackFromFilePath(URL(mediaFile), fromDAW); + + if (! showMediaClipboard) + { + viewMediaClipboardCallback(); + } +} + +inline void MainComponent::openFileChooser() +{ + StringArray validExtensions = MediaDisplayComponent::getSupportedExtensions(); + String filePatternsAllowed = "*" + validExtensions.joinIntoString(";*"); + + openFileBrowser = + std::make_unique("Select a media file...", File(), filePatternsAllowed); + + openFileBrowser->launchAsync(FileBrowserComponent::openMode + | FileBrowserComponent::canSelectFiles, + [this](const FileChooser& browser) + { + File chosenFile = browser.getResult(); + if (chosenFile != File {}) + { + importNewFile(chosenFile); + } + }); +} + +inline Rectangle MainComponent::getProcessButtonBounds() +{ + return processCancelButton.getBounds(); +} + +inline Rectangle MainComponent::getTracksBounds() +{ + auto bounds = inputTrackAreaWidget.getBounds(); + if (outputTrackAreaWidget.isVisible()) + bounds = bounds.getUnion(outputTrackAreaWidget.getBounds()); + + // Include labels if visible + if (inputTracksLabel.isVisible()) + bounds = bounds.getUnion(inputTracksLabel.getBounds()); + if (outputTracksLabel.isVisible()) + bounds = bounds.getUnion(outputTracksLabel.getBounds()); + + return bounds.expanded(5, 5); +} + +inline Rectangle MainComponent::getInfoBarBounds() +{ + auto bounds = instructionBox->getBounds(); + if (statusBox->isVisible()) + bounds = bounds.getUnion(statusBox->getBounds()); + return bounds.expanded(5, 5); +} + +inline Rectangle MainComponent::getClipboardBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + return mediaClipboardWidget.getBounds().expanded(5, 5); + return {}; +} diff --git a/src/WebModel.h b/src/WebModel.h index 4f901c33..878ac392 100644 --- a/src/WebModel.h +++ b/src/WebModel.h @@ -22,7 +22,7 @@ class WebModel : public Model public: WebModel() { status2 = ModelStatus::INITIALIZED; } - ~WebModel() {} + virtual ~WebModel() {} bool ready() const override { return m_loaded; } diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 9529fe36..3f2c62c9 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -111,6 +111,20 @@ class MediaDisplayComponent : public Component, virtual bool isPlaying() { return transportSource.isPlaying(); } + Rectangle getChooseFileButtonBounds() + { + if (auto* p = chooseFileButton.getParentComponent()) + return getLocalArea(p, chooseFileButton.getBounds()); + return chooseFileButton.getBounds(); + } + + Rectangle getPlayButtonBounds() + { + if (auto* p = playStopButton.getParentComponent()) + return getLocalArea(p, playStopButton.getBounds()); + return playStopButton.getBounds(); + } + int getNumOverheadLabels(); void addOverheadLabel(OverheadLabelComponent* l); diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index cd66a868..10f69a5d 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -110,6 +110,30 @@ class TrackAreaWidget : public Component, return nullptr; } + Rectangle getFirstTrackFolderButtonBounds() + { + if (mediaDisplays.size() > 0) + { + auto display = mediaDisplays[0].get(); + auto bounds = display->getChooseFileButtonBounds(); + // Convert to TrackAreaWidget coordinates + return getLocalArea(display, bounds); + } + return {}; + } + + Rectangle getFirstTrackPlayButtonBounds() + { + if (mediaDisplays.size() > 0) + { + auto display = mediaDisplays[0].get(); + auto bounds = display->getPlayButtonBounds(); + // Convert to TrackAreaWidget coordinates + return getLocalArea(display, bounds); + } + return {}; + } + std::vector getDAWLinkedDisplays() { std::vector linkedDisplays; diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 1881d0c7..8b9b1dae 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -2,7 +2,9 @@ #include "../AppSettings.h" #include "../MainComponent.h" -#include +#include +#include +#include using namespace juce; @@ -11,9 +13,10 @@ struct TutorialStep String title; String description; std::function(MainComponent*)> getHighlightBounds; + std::function>(MainComponent*)> getExtraHighlights = nullptr; }; -class WelcomeWindow : public DocumentWindow +class WelcomeWindow : public DocumentWindow, public ChangeListener { public: WelcomeWindow(MainComponent* mainComp) @@ -28,58 +31,42 @@ class WelcomeWindow : public DocumentWindow setSize(500, 420); setAlwaysOnTop(true); - // Define steps - steps = { - { "Welcome to HARP", - "HARP is your hub for hosted, asynchronous, remote audio processing.\n\n" - "HARP operates as a standalone app or plugin-like editor within your DAW, allowing you to process tracks using ML models hosted on platforms like Hugging Face or Stability AI.\n\n" - "This quick tutorial will guide you through the main features of the application.", - [](MainComponent*) { return Rectangle(); } }, - - { "Select a Model", - "Start by choosing a model from the dropdown list. We support HuggingSpace, Stability AI, and custom endpoints.\n\n" - "Click 'Load' to initialize the selected model.", - [](MainComponent* c) { return c->getModelSelectBounds(); } }, - - { "Configure Parameters", - "Adjust the model parameters here. These controls change dynamically based on the selected model.\n\n" - "You can hover over the components to view its description below in the status bar.", - [](MainComponent* c) { return c->getControlsBounds(); } }, - - { "Manage Tracks", - "Select or drag and drop your audio or MIDI files onto the input tracks.\n" - "Processed results will appear in the output tracks.", - [](MainComponent* c) { return c->getTracksBounds(); } }, - - { "Track Status", - "\nKeep an eye on the bottom bar for status updates, processing progress, and helpful instructions.", - [](MainComponent* c) { return c->getInfoBarBounds(); } }, - - { "Media Clipboard", - "Use the right-hand clipboard to manage your media files. You can drag processed files back to your DAW or reuse them as inputs.", - [](MainComponent* c) { return c->getClipboardBounds(); } }, + if (mainComponent) + { + mainComponent->getLoadBroadcaster().addChangeListener(this); + mainComponent->setTutorialActive(true); + } - { "All Set!", - "You're ready to start creating!\n\n" - "Don't forget to set up your API tokens in the Settings menu if you plan to use hosted models.\n" - "Add tokens under File -> Settings -> Hugging Face", - [](MainComponent*) { return Rectangle(); } } - }; + // Initial Build + rebuildSteps(); // UI Init addAndMakeVisible(&titleLabel); titleLabel.setFont(Font(24.0f, Font::bold)); titleLabel.setJustificationType(Justification::centred); - addAndMakeVisible(&descriptionLabel); - descriptionLabel.setFont(Font(16.0f)); - descriptionLabel.setJustificationType(Justification::centredTop); + addAndMakeVisible(&descriptionEditor); + descriptionEditor.setMultiLine(true); + descriptionEditor.setReadOnly(true); + descriptionEditor.setScrollbarsShown(true); + descriptionEditor.setCaretVisible(false); + // Make it look like a label (transparent) + descriptionEditor.setColour(TextEditor::backgroundColourId, Colours::transparentBlack); + descriptionEditor.setColour(TextEditor::outlineColourId, Colours::transparentBlack); + descriptionEditor.setFont(Font(16.0f)); addAndMakeVisible(&learnMoreLink); learnMoreLink.setButtonText("Learn more"); learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); learnMoreLink.setColour(HyperlinkButton::textColourId, Colours::skyblue); + // pyHarpLink init removed + + // Step 2 Labels init + // hostingEditor init removed + + // endpointLabel removed + addAndMakeVisible(©rightLabel); copyrightLabel.setText("Copyright 2025 TEAMuP. All rights reserved.", dontSendNotification); copyrightLabel.setJustificationType(Justification::centred); @@ -88,7 +75,11 @@ class WelcomeWindow : public DocumentWindow addAndMakeVisible(&nextButton); nextButton.setButtonText("Next"); - nextButton.onClick = [this] { nextStep(); }; + nextButton.onClick = [this] + { + DBG("Next Button Clicked"); + nextStep(); + }; addAndMakeVisible(&prevButton); prevButton.setButtonText("Back"); @@ -105,17 +96,336 @@ class WelcomeWindow : public DocumentWindow addAndMakeVisible(&pageIndicator); pageIndicator.setJustificationType(Justification::centred); pageIndicator.setFont(Font(12.0f)); + pageIndicator.setInterceptsMouseClicks(false, false); + + addAndMakeVisible(&showDetailsButton); + showDetailsButton.setButtonText("Show detailed control descriptions"); + showDetailsButton.onClick = [this] + { + showingDetails = ! showingDetails; + showDetailsButton.setButtonText(showingDetails ? "Hide detailed control descriptions" + : "Show detailed control descriptions"); + rebuildSteps(); + updateStep(); + }; // Initial Update updateStep(); setVisible(true); } + ~WelcomeWindow() override + { + if (mainComponent) + { + mainComponent->getLoadBroadcaster().removeChangeListener(this); + mainComponent->setTutorialActive(false); + } + } + + void changeListenerCallback(ChangeBroadcaster*) override + { + // Model loaded/changed + rebuildSteps(); + + if (currentStep > 0) + { + // Auto-jump to Step 3 ("Quick Start") if user loads a new model + // (Index 2 corresponds to "Quick Start") + if (steps.size() > 2) + { + currentStep = 2; + } + updateStep(); + } + } + + void rebuildSteps() + { + steps.clear(); + + // 1. Static Intro + steps.push_back( + { "Welcome to HARP", + "HARP is your hub for hosted, asynchronous, remote audio processing.\n\n" + "HARP operates as a standalone app or plugin-like editor within your DAW, allowing you to process tracks using ML models hosted on platforms like Hugging Face or Stability AI.\n\n" + "This quick tutorial will guide you through the main features of the application.", + [](MainComponent*) { return Rectangle(); } }); + + // 2. Select Model + steps.push_back( + { "Select a Model", + "Select a model from the dropdown at the top and click Load.\n\n" + "Once loaded, the model's details and controls will appear below.\n\n" + "If you don't select another model, HARP uses the Demucs Stem Separator by default.", + [](MainComponent* c) { return c->getModelSelectBounds(); } }); + + // 3. Model Overview (Dynamic) + String modelName = "Demucs Source Separation"; // Default + String modelId = "demucs"; + + if (mainComponent) + { + auto model = mainComponent->getModel(); + if (model && model->ready()) + { + modelName = model->card().name; + modelId = modelName; // Use full name for matching + } + } + + // 3. Quick Start (Dynamic) + String stepTitle = "Quick Start"; + + steps.push_back( + { stepTitle, + "This is the " + modelName + + ".\n\n" + "1. Add input\n" + "Drag an audio or MIDI file into the Input track, use the folder icon to browse, or the play button to preview.\n\n" + "2. Process\n" + "Click Process to run the model.\n\n" + "3. Output\n" + "Processed tracks appear in the Output section, where you can play or save them.", + [](MainComponent* c) + { + auto bounds = c->getLocalBounds(); + auto clipboard = c->getClipboardBounds(); + if (! clipboard.isEmpty()) + { + // Exclude clipboard (assume it's on the right) + bounds.setRight(clipboard.getX()); + } + return bounds; + }, // Highlight entire interface except clipboard + [](MainComponent* c) + { + std::vector> v; + v.push_back(c->getInputFolderBounds()); + v.push_back(c->getInputPlayBounds()); + v.push_back(c->getProcessButtonBounds()); + return v; + } }); + + // 4. Configure Parameters (Dynamic) + // 4. Configure Parameters (Dynamic) + if (mainComponent) + { + auto model = mainComponent->getModel(); + if (model && model->ready()) + { + String stepTitle = "Configure Parameters (Optional)"; + String baseText = + "These settings let you customize how the model behaves. The default values are recommended for first-time use. What these controls do (at a high level):\n\n" + "1. Quality vs Speed\n" + "Adjusts the tradeoff between faster processing and higher-quality results.\n\n" + "2. Model Variants\n" + "Some models offer different variants optimized for different use cases.\n\n" + "3. Advanced Options\n" + "Fine-tuning controls for experienced users who want more control over the output.\n\n"; + + String fullText = baseText; + + if (showingDetails) + { + fullText += + "--------------------------------------------------\nDetailed Control Descriptions:\n\n"; + auto& controls = model->getControlsInfo(); + if (controls.empty()) + { + fullText += "- No adjustable controls for this model."; + } + else + { + int index = 1; + for (const auto& pair : controls) + { + auto info = pair.second; + String friendlyCtrlDesc = getFriendlyControlDescription( + model->card().name, info->label, info->info); + + fullText += String(index++) + ". " + info->label + ": " + + friendlyCtrlDesc + "\n\n"; + } + } + fullText += + "Tip: We suggest sticking with the default parameters for the best balance of processing time vs. quality."; + } + + steps.push_back({ stepTitle, fullText, [](MainComponent* c) { + return c->getControlsBounds(); + } }); + } + } + + // 5. Manage Tracks + steps.push_back( + { "Manage Tracks", + "The highlighted panel contains your input and output tracks.\n\n" + "You can drag and drop audio/MIDI files here, or click on the folder icon in the input audio section to choose from a local folder.\n\n" + "Processed results will appear in the output tracks section, which you can play here or save directly.\n\n" + "Please note that these tracks interact with the model.", + [](MainComponent* c) { return c->getTracksBounds(); }, + nullptr }); + + steps.push_back( + { "Track Status", + "The bottom bar shows the connection and processing status.\n\n" + "LOADED: The model is ready to use.\n\n" + "PROCESSING: The model is busy working. Please wait.\n\n" + "ERROR: Something went wrong. Check the description below it and try reloading the model.", + [](MainComponent* c) { return c->getInfoBarBounds(); }, + nullptr }); + + steps.push_back( + { "Media Clipboard", + "This clipboard is a scratch space. Tracks here do not interact with the models directly.\n\n" + "However, you can stash model outputs here to reuse them later.", + [](MainComponent* c) { return c->getClipboardBounds(); }, + nullptr }); + + // 8. Interface Summary + steps.push_back( + { "Interface Summary", + "Top Bar: Select a model from the dropdown and click Load to initialize it.\n\n" + "Left Panel: This is where your Input and Output tracks live. Models read from and write to these tracks.\n\n" + "Right Panel: A scratch pad (Clipboard) to stash tracks you want to save or reuse later.", + [](MainComponent* c) { return Rectangle(); }, + [](MainComponent* c) + { + std::vector> v; + v.push_back(c->getModelSelectBounds()); + v.push_back(c->getInputTrackBounds()); + v.push_back(c->getClipboardBounds()); + return v; + } }); + + steps.push_back( + { "All Set!", + "Most models in HARP are hosted on Hugging Face and Stability AI. To use them, you will need API tokens.\n\n" + "You can get your tokens at https://huggingface.co/settings/tokens\n\n" + "Once you have them, please add them under 'File -> Settings' to start creating!", + [](MainComponent*) { return Rectangle(); } }); + + // Refresh if we are showing the tutorial + if (isVisible()) + { + if (currentStep >= (int) steps.size()) + currentStep = (int) steps.size() - 1; + updateStep(); + } + } + + String getFriendlyModelOverview(const String& name) + { + // DEMUCS (Default or Selected) + if (name.containsIgnoreCase("demucs")) + { + return "About:\n" + "The Demucs model is a state-of-the-art music source separation model. " + "It separates a stereo specific mixture into four distinct tracks: Drums, Bass, Vocals, and Other instruments.\n\n" + "Inputs:\n" + "A generic audio file (mp3, wav, etc.) containing a song or music mix.\n\n" + "Outputs:\n" + "Four separate audio tracks corresponding to the separated stems."; + } + + // MEGATTS / Voice + if (name.containsIgnoreCase("megatts") || name.containsIgnoreCase("voice")) + { + return "About:\n" + "This model generates realistic speech or clones voices from reference audio.\n\n" + "Inputs:\n" + "Reference audio of the voice to clone and the text you want it to speak.\n\n" + "Outputs:\n" + "A generated audio clip of the spoken text."; + } + + // MUSICGEN / Audio Generation + if (name.containsIgnoreCase("musicgen") || name.containsIgnoreCase("audioldm")) + { + return "About:\n" + "Generates new music or audio based on text descriptions.\n\n" + "Inputs:\n" + "A text prompt describing the music (e.g., 'lo-fi beat with piano').\n\n" + "Outputs:\n" + "A generated audio track matching the description."; + } + + // Fallback + return "About:\n" + "This is an advanced audio processing model hosted in the cloud.\n\n" + "Inputs:\n" + "Refer to the specific model documentation or parameters for input requirements.\n\n" + "Outputs:\n" + "Processed audio or MIDI results."; + } + + String getFriendlyModelDescription(const String& name, const String& rawDesc) + { + // Unused but kept for API consistency if needed later + return rawDesc; + } + + String getFriendlyControlDescription(const String& modelName, + const String& label, + const String& rawInfo) + { + // DEMUCS + if (modelName.containsIgnoreCase("demucs")) + { + if (label.equalsIgnoreCase("shifts")) + { + // Roman numerals for sub-points + return "Determines how many times the audio is shifted and processed.\n" + " i. Increasing this value can improve quality but will take longer to process.\n" + " ii. A value of 0 or 1 is usually sufficient for good results."; + } + if (label.equalsIgnoreCase("overlap")) + { + return "Controls the smoothness between processed audio chunks. The default value is optimized for a seamless sound."; + } + } + + // MEGATTS 3 + if (modelName.containsIgnoreCase("megatts")) + { + if (label.containsIgnoreCase("timesteps")) + { + return "Controls the quality of the voice generation.\n" + " i. Higher values (e.g. 50+) result in clearer, higher-quality audio but take longer to generate."; + } + if (label.containsIgnoreCase("intelligibility")) + { + return "Ensures the generated speech is clear and easy to understand.\n" + " i. Increase this value if the output sounds mumbled or unclear."; + } + if (label.containsIgnoreCase("similarity")) + { + return "Controls how closely the cloned voice mimics the reference audio.\n" + " i. Increase this for a stronger likeness to the original voice."; + } + if (label.containsIgnoreCase("text")) + { + return "Enter the text you want the cloned voice to speak."; + } + } + + // GENERIC FALLBACK + if (rawInfo.isNotEmpty()) + return rawInfo; + + return "Controls this specific parameter of the model."; + } + void closeButtonPressed() override { - // Reset highlight before closing + // Reset state before closing if (mainComponent) + { mainComponent->setTutorialHighlight({}); + mainComponent->setTutorialActive(false); + } if (onClose) onClose(); @@ -139,50 +449,49 @@ class WelcomeWindow : public DocumentWindow // Footer Buttons auto footer = area.removeFromBottom(30); auto buttonWidth = 100; - skipButton.setBounds(footer.removeFromLeft(buttonWidth)); - nextButton.setBounds(footer.removeFromRight(buttonWidth)); footer.removeFromRight(10); prevButton.setBounds(footer.removeFromRight(buttonWidth)); - pageIndicator.setBounds(footer); - // Spacer above footer - area.removeFromBottom(10); + area.removeFromBottom(10); // Spacer if (currentStep == 0) { - // Step 1 Specific Layout - - // Copyright remains at the very bottom copyrightLabel.setBounds(area.removeFromBottom(20)); - - // Layout from top down to keep Link close to Text - - // Description Area - explicit height to contain the text without massive gap - // 3 paragraphs of text at ~16px font - descriptionLabel.setBounds(area.removeFromTop(200)); - - // Link directly below description + descriptionEditor.setBounds(area.removeFromTop(200)); auto linkArea = area.removeFromTop(30); learnMoreLink.setBounds(linkArea.reduced(linkArea.getWidth() / 2 - 50, 0)); - - // Remaining area is empty space between link and copyright } else { - // Standard Layout for other steps - - // Checkbox area (only visible on last page) if (currentStep == (int) steps.size() - 1) { auto checkArea = area.removeFromBottom(30); dontShowAgainToggle.setBounds(checkArea.removeFromRight(200)); } - // Description fills the rest - descriptionLabel.setBounds(area); + descriptionEditor.setBounds(area); + + if (showDetailsButton.isVisible()) + { + auto btnArea = area.removeFromBottom(30); + showDetailsButton.setBounds(btnArea.reduced(20, 0)); + // Recalculate description bounds if details button taking space? + // Wait, descriptionEditor.setBounds(area) was called above. + // If details button is visible, we need to shrink description editor further? + // The original logic was: + // descriptionEditor.setBounds(area); + // if (showDetailsButton...) { btnArea = area.removeFromBottom... desc.setBounds(...) } + // My new logic: + // descriptionEditor assigned to 'area'. + // If showDetailsButton is visible (on Step 4), 'area' should be reduced? + // Actually original logic was: + // descriptionEditor.setBounds(area); + // if (showDetailsButton...) { ... descriptionEditor.setBounds(area.removeFromTop...)} + // So I should keep that flow. + } } } @@ -195,13 +504,23 @@ class WelcomeWindow : public DocumentWindow const auto& step = steps[size_t(currentStep)]; titleLabel.setText(step.title, dontSendNotification); - descriptionLabel.setText(step.description, dontSendNotification); + descriptionEditor.setText(step.description); + // Scroll to top when changing steps + descriptionEditor.setCaretPosition(0); // Highlight logic - if (mainComponent) { auto bounds = step.getHighlightBounds(mainComponent); mainComponent->setTutorialHighlight(bounds); + + if (step.getExtraHighlights) + { + mainComponent->setTutorialExtraHighlights(step.getExtraHighlights(mainComponent)); + } + else + { + mainComponent->setTutorialExtraHighlights({}); + } } // Buttons @@ -209,6 +528,7 @@ class WelcomeWindow : public DocumentWindow bool isLast = currentStep == (int) steps.size() - 1; bool isFirst = currentStep == 0; + bool isSecond = currentStep == 1; // "Select a Model" nextButton.setButtonText(isLast ? "Finish" : "Next"); skipButton.setVisible(! isLast); @@ -218,10 +538,18 @@ class WelcomeWindow : public DocumentWindow learnMoreLink.setVisible(isFirst); copyrightLabel.setVisible(isFirst); + // Item specific to second page (Step 2) + // hostingEditor removed + // pyHarpLink removed + pageIndicator.setText("Step " + String(currentStep + 1) + " of " + String(steps.size()), dontSendNotification); - // Trigger layout update since we change component visibility and position logic + // Show details button only on Configure Parameters step + bool isConfigParams = step.title.contains("Configure Parameters"); + showDetailsButton.setVisible(isConfigParams); + + // Trigger layout update resized(); } @@ -269,14 +597,21 @@ class WelcomeWindow : public DocumentWindow int currentStep = 0; Label titleLabel; - Label descriptionLabel; + TextEditor descriptionEditor; // Changed from Label + + // Step 2 specific labels removed + TextButton nextButton; TextButton prevButton; TextButton skipButton; ToggleButton dontShowAgainToggle; Label pageIndicator; HyperlinkButton learnMoreLink; + // HyperlinkButton pyHarpLink; // Removed Label copyrightLabel; + TextButton showDetailsButton; + bool showingDetails = false; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WelcomeWindow) }; \ No newline at end of file From 121fa4fffe3b3c149c4787ca2a13311bd3fd5ab3 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Thu, 8 Jan 2026 17:24:13 -0600 Subject: [PATCH 03/13] stability models desc fix --- src/windows/WelcomeWindow.h | 75 ++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 8b9b1dae..3599e03a 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -238,8 +238,13 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener else { int index = 1; + + // First pass: All except Model for (const auto& pair : controls) { + if (String(pair.second->label).equalsIgnoreCase("Model")) + continue; + auto info = pair.second; String friendlyCtrlDesc = getFriendlyControlDescription( model->card().name, info->label, info->info); @@ -247,9 +252,23 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener fullText += String(index++) + ". " + info->label + ": " + friendlyCtrlDesc + "\n\n"; } + + // Second pass: Only Model + for (const auto& pair : controls) + { + if (! String(pair.second->label).equalsIgnoreCase("Model")) + continue; + + auto info = pair.second; + String friendlyCtrlDesc = getFriendlyControlDescription( + model->card().name, info->label, info->info); + + fullText += String(index++) + ". " + info->label + ": " + + friendlyCtrlDesc + "\n\n"; + } + fullText += + "Tip: We suggest sticking with the default parameters for the best balance of processing time vs. quality."; } - fullText += - "Tip: We suggest sticking with the default parameters for the best balance of processing time vs. quality."; } steps.push_back({ stepTitle, fullText, [](MainComponent* c) { @@ -387,6 +406,28 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } + // STABILITY AI (Text-to-Audio / Audio-to-Audio) + if (modelName.containsIgnoreCase("stability") + || modelName.containsIgnoreCase("text-to-audio") + || modelName.containsIgnoreCase("audio-to-audio") + || modelName.containsIgnoreCase("stable audio")) + { + if (label.containsIgnoreCase("Duration")) + return "Sets the length of the generated audio in seconds."; + + if (label.containsIgnoreCase("steps")) + return "Number of diffusion steps. Higher values improve quality but take longer to process."; + + if (label.containsIgnoreCase("cfg")) + return "Classifier Free Guidance. Higher values make the output adhere more strictly to the prompt."; + + if (label.containsIgnoreCase("Output Format")) + return "The file format for the generated audio (e.g. wav)."; + + if (label.containsIgnoreCase("Text Prompt")) + return "Description of the audio content you want to generate."; + } + // MEGATTS 3 if (modelName.containsIgnoreCase("megatts")) { @@ -411,6 +452,15 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } + // TRIA + if (modelName.containsIgnoreCase("tria")) + { + if (label.equalsIgnoreCase("Model")) + { + return "Choose which variant to you want to use. Currently this is the only one available, however more models would be available soon."; + } + } + // GENERIC FALLBACK if (rawInfo.isNotEmpty()) return rawInfo; @@ -472,26 +522,17 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener dontShowAgainToggle.setBounds(checkArea.removeFromRight(200)); } - descriptionEditor.setBounds(area); - + // Fix for UI overlap: Reserve space for the "show details" button at the bottom FIRST if (showDetailsButton.isVisible()) { auto btnArea = area.removeFromBottom(30); showDetailsButton.setBounds(btnArea.reduced(20, 0)); - // Recalculate description bounds if details button taking space? - // Wait, descriptionEditor.setBounds(area) was called above. - // If details button is visible, we need to shrink description editor further? - // The original logic was: - // descriptionEditor.setBounds(area); - // if (showDetailsButton...) { btnArea = area.removeFromBottom... desc.setBounds(...) } - // My new logic: - // descriptionEditor assigned to 'area'. - // If showDetailsButton is visible (on Step 4), 'area' should be reduced? - // Actually original logic was: - // descriptionEditor.setBounds(area); - // if (showDetailsButton...) { ... descriptionEditor.setBounds(area.removeFromTop...)} - // So I should keep that flow. + // Add some spacing between button and description + area.removeFromBottom(10); } + + // Finally, the description editor takes the remaining space + descriptionEditor.setBounds(area); } } From 60d36738679473f9827fa5a824bbcc393e51d50f Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Mon, 19 Jan 2026 11:39:58 -0600 Subject: [PATCH 04/13] review changes --- src/windows/WelcomeWindow.h | 51 +++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 3599e03a..fa65400b 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -149,13 +149,13 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { "Welcome to HARP", "HARP is your hub for hosted, asynchronous, remote audio processing.\n\n" "HARP operates as a standalone app or plugin-like editor within your DAW, allowing you to process tracks using ML models hosted on platforms like Hugging Face or Stability AI.\n\n" - "This quick tutorial will guide you through the main features of the application.", + "This quick tutorial will guide you through the main features of the application. Click 'Learn more' for additional online documentation.", [](MainComponent*) { return Rectangle(); } }); // 2. Select Model steps.push_back( { "Select a Model", - "Select a model from the dropdown at the top and click Load.\n\n" + "Select a model from the dropdown menu at the top and click Load.\n\n" "Once loaded, the model's details and controls will appear below.\n\n" "If you don't select another model, HARP uses the Demucs Stem Separator by default.", [](MainComponent* c) { return c->getModelSelectBounds(); } }); @@ -179,8 +179,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener steps.push_back( { stepTitle, - "This is the " + modelName - + ".\n\n" + "This is the " + modelName + ".\n" + getFriendlyModelSummary(modelName) + + "\n\n" "1. Add input\n" "Drag an audio or MIDI file into the Input track, use the folder icon to browse, or the play button to preview.\n\n" "2. Process\n" @@ -216,13 +216,9 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { String stepTitle = "Configure Parameters (Optional)"; String baseText = - "These settings let you customize how the model behaves. The default values are recommended for first-time use. What these controls do (at a high level):\n\n" - "1. Quality vs Speed\n" - "Adjusts the tradeoff between faster processing and higher-quality results.\n\n" - "2. Model Variants\n" - "Some models offer different variants optimized for different use cases.\n\n" - "3. Advanced Options\n" - "Fine-tuning controls for experienced users who want more control over the output.\n\n"; + "Some models will have controls to customize model behavior. Typical controls include ones that balance quality vs speed, allow selection of model variants, or provide advanced options for experienced users.\n\n" + "To see descriptions of what controls do for a model Click on the button below. Do this now to see the control descriptions for " + + modelName + ".\n\n"; String fullText = baseText; @@ -321,9 +317,10 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener steps.push_back( { "All Set!", - "Most models in HARP are hosted on Hugging Face and Stability AI. To use them, you will need API tokens.\n\n" - "You can get your tokens at https://huggingface.co/settings/tokens\n\n" - "Once you have them, please add them under 'File -> Settings' to start creating!", + "Most models in HARP are hosted on external service like Hugging Face and Stability AI. To use them, you will need API tokens.\n\n" + "An API token is a private access key that lets HARP securely connect to these services on your behalf. It tells the service that you are allowed to use the model\n\n" + "You can get these tokens from your account settings on the respective service websites. You can find the exact link in 'File -> Settings' to help you add your token.\n\n" + "You are now ready to start creating!", [](MainComponent*) { return Rectangle(); } }); // Refresh if we are showing the tutorial @@ -335,6 +332,28 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } + String getFriendlyModelSummary(const String& name) + { + if (name.containsIgnoreCase("demucs")) + { + return "It separates music audio into drums, bass, vocals, and 'other' stems."; + } + if (name.containsIgnoreCase("megatts") || name.containsIgnoreCase("voice")) + { + return "It generates realistic speech or clones voices from reference audio."; + } + if (name.containsIgnoreCase("musicgen") || name.containsIgnoreCase("audioldm")) + { + return "It generates new music or audio based on text descriptions."; + } + if (name.containsIgnoreCase("stability") || name.containsIgnoreCase("stable audio")) + { + return "It generates or transforms audio using state-of-the-art diffusion models."; + } + + return "It is an advanced audio processing model hosted in the cloud."; + } + String getFriendlyModelOverview(const String& name) { // DEMUCS (Default or Selected) @@ -380,7 +399,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener "Processed audio or MIDI results."; } - String getFriendlyModelDescription(const String& name, const String& rawDesc) + String getFriendlyModelDescription(const String& /*name*/, const String& rawDesc) { // Unused but kept for API consistency if needed later return rawDesc; @@ -569,7 +588,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener bool isLast = currentStep == (int) steps.size() - 1; bool isFirst = currentStep == 0; - bool isSecond = currentStep == 1; // "Select a Model" + // bool isSecond = currentStep == 1; // "Select a Model" nextButton.setButtonText(isLast ? "Finish" : "Next"); skipButton.setVisible(! isLast); From 000235c6462b6cc744e11d65b55cebc4f46d5d16 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 23 Jan 2026 13:52:08 -0600 Subject: [PATCH 05/13] modified comments --- src/windows/WelcomeWindow.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index fa65400b..ddc3e0b8 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -182,7 +182,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener "This is the " + modelName + ".\n" + getFriendlyModelSummary(modelName) + "\n\n" "1. Add input\n" - "Drag an audio or MIDI file into the Input track, use the folder icon to browse, or the play button to preview.\n\n" + "Drag an audio file into the Input track, use the folder icon to browse, or the play button to preview.\n\n" "2. Process\n" "Click Process to run the model.\n\n" "3. Output\n" @@ -295,7 +295,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener steps.push_back( { "Media Clipboard", "This clipboard is a scratch space. Tracks here do not interact with the models directly.\n\n" - "However, you can stash model outputs here to reuse them later.", + "However, you can stash model outputs here to reuse them later.\n\n" + "To stash the input or output track in the clipboard, drag the track here to re-use it later.", [](MainComponent* c) { return c->getClipboardBounds(); }, nullptr }); @@ -318,8 +319,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener steps.push_back( { "All Set!", "Most models in HARP are hosted on external service like Hugging Face and Stability AI. To use them, you will need API tokens.\n\n" - "An API token is a private access key that lets HARP securely connect to these services on your behalf. It tells the service that you are allowed to use the model\n\n" - "You can get these tokens from your account settings on the respective service websites. You can find the exact link in 'File -> Settings' to help you add your token.\n\n" + "An API token is a private access key that lets HARP securely connect to these services on your behalf. It tells the service that you are allowed to use the model.\n\n" + "You can get these tokens from your account settings on the respective service websites. You can find the exact link in 'File -> Settings' and select the tab of the service whose tokens you need to use.\n\n" "You are now ready to start creating!", [](MainComponent*) { return Rectangle(); } }); From 236e41c9d23dd3ae28c208e3eb38e262f03955a9 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 23 Jan 2026 14:27:58 -0600 Subject: [PATCH 06/13] reshow guide in about --- src/Main.cpp | 50 +++++++++++++++++++++++++-------------- src/MainComponent.h | 15 ++++++++++++ src/windows/AboutWindow.h | 40 ++++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 88ffb47e..eeda2264 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -4,6 +4,9 @@ using namespace juce; +// Define the static callback member from MainComponent +std::function MainComponent::showTutorialCallback = nullptr; + class GuiAppApplication : public JUCEApplication { public: @@ -75,26 +78,12 @@ class GuiAppApplication : public JUCEApplication if (showWelcome) { - MessageManager::callAsync( - [this]() - { - auto* mainComp = - dynamic_cast(mainWindow->getContentComponent()); - if (mainComp) - { - welcomeWindow.reset(new WelcomeWindow(mainComp)); - - // Handle window closing - welcomeWindow->onClose = [this]() { welcomeWindow.reset(); }; - - // Position relative to main window or center - welcomeWindow->centreWithSize(500, 420); - welcomeWindow->setVisible(true); - welcomeWindow->toFront(true); - } - }); + showWelcomeWindow(); } + // Set up the static callback for MainComponent to trigger Welcome window + MainComponent::showTutorialCallback = [this]() { showWelcomeWindow(); }; + StringArray args; // Split command line arguments at spaces @@ -197,6 +186,31 @@ class GuiAppApplication : public JUCEApplication */ void systemRequestedQuit() override { quit(); } + /** + * Public method to show the Welcome/Tutorial window. + * Called from MainComponent when user clicks "Show Tutorial" in About. + */ + void showWelcomeWindow() + { + MessageManager::callAsync( + [this]() + { + auto* mainComp = dynamic_cast(mainWindow->getContentComponent()); + if (mainComp) + { + welcomeWindow.reset(new WelcomeWindow(mainComp)); + + // Handle window closing + welcomeWindow->onClose = [this]() { welcomeWindow.reset(); }; + + // Position relative to main window or center + welcomeWindow->centreWithSize(500, 420); + welcomeWindow->setVisible(true); + welcomeWindow->toFront(true); + } + }); + } + /** * Implements desktop window that contains instance of MainComponent. */ diff --git a/src/MainComponent.h b/src/MainComponent.h index 08860e93..cf21df20 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -165,6 +165,9 @@ class MainComponent : public Component, { auto aboutComponent = std::make_unique(); + // Set up the tutorial callback before releasing ownership + aboutComponent->onShowTutorial = [this]() { showTutorialFromAbout(); }; + DialogWindow::LaunchOptions dialog; dialog.content.setOwned(aboutComponent.release()); dialog.dialogTitle = "About " + String(APP_NAME); @@ -176,6 +179,18 @@ class MainComponent : public Component, dialog.launchAsync(); } + void showTutorialFromAbout() + { + // Trigger the static callback if it's been set by the application + if (showTutorialCallback) + { + showTutorialCallback(); + } + } + + // Static callback that the application sets to trigger the welcome window + static std::function showTutorialCallback; + void undoCallback(); void redoCallback(); diff --git a/src/windows/AboutWindow.h b/src/windows/AboutWindow.h index e53d2130..6664f48a 100644 --- a/src/windows/AboutWindow.h +++ b/src/windows/AboutWindow.h @@ -1,6 +1,7 @@ #pragma once #include "juce_gui_basics/juce_gui_basics.h" +#include using namespace juce; @@ -9,7 +10,7 @@ class AboutWindow : public Component public: AboutWindow() { - setSize(400, 300); + setSize(400, 340); // label for the about text aboutText.setText(String(APP_NAME) + "\nVersion: " + String(APP_VERSION) + "\n\n", @@ -20,7 +21,8 @@ class AboutWindow : public Component // hyperlink buttons modelGlossaryButton.setButtonText("Model Glossary"); - modelGlossaryButton.setURL(URL("https://harp-plugin.netlify.app/content/usage/models.html")); + modelGlossaryButton.setURL( + URL("https://harp-plugin.netlify.app/content/usage/models.html")); modelGlossaryButton.setSize(380, 24); modelGlossaryButton.setTopLeftPosition(10, 110); modelGlossaryButton.setColour(HyperlinkButton::textColourId, Colours::blue); @@ -44,14 +46,46 @@ class AboutWindow : public Component copyrightLabel.setText(String(APP_COPYRIGHT) + "\n\n", dontSendNotification); copyrightLabel.setJustificationType(Justification::centred); copyrightLabel.setSize(380, 100); - copyrightLabel.setTopLeftPosition(10, 200); + copyrightLabel.setTopLeftPosition(10, 240); addAndMakeVisible(copyrightLabel); + + // Show Tutorial button + showTutorialButton.setButtonText("Show Tutorial"); + showTutorialButton.setSize(380, 24); + showTutorialButton.setTopLeftPosition(10, 200); + showTutorialButton.onClick = [this]() + { + // Save the callback before we close the window + auto callback = onShowTutorial; + + // Find and close the parent dialog window + if (auto* dialogWindow = findParentComponentOfClass()) + { + dialogWindow->exitModalState(0); + + // Use async call to trigger tutorial after dialog closes + MessageManager::callAsync( + [callback]() + { + if (callback) + callback(); + }); + } + else if (callback) + { + callback(); + } + }; + addAndMakeVisible(showTutorialButton); } + std::function onShowTutorial; + private: Label aboutText; HyperlinkButton modelGlossaryButton; HyperlinkButton visitWebpageButton; HyperlinkButton reportIssueButton; + TextButton showTutorialButton; Label copyrightLabel; }; From 8b5bedfd62db00b4d50b8cb3e17e12df679a521b Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 6 Feb 2026 12:00:37 -0600 Subject: [PATCH 07/13] build fixes --- src/MainComponent.cpp | 72 +++++++++++++++++-------------------- src/MainComponent.h | 32 ++++++++++------- src/ModelTab.h | 45 +++++++++++++++++++++++ src/windows/WelcomeWindow.h | 30 ++++++++-------- 4 files changed, 113 insertions(+), 66 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 179f797c..f54c021b 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -1,4 +1,5 @@ #include "MainComponent.h" +#include "windows/WelcomeWindow.h" JUCE_IMPLEMENT_SINGLETON(HARPLogger) @@ -300,7 +301,7 @@ void MainComponent::openWelcomeWindow() MessageManager::callAsync( [this]() { - welcomeWindow.reset(new WelcomeWindow(mainComp)); + welcomeWindow.reset(new WelcomeWindow(this)); // Handle window closing welcomeWindow->onClose = [this]() { welcomeWindow.reset(); }; @@ -312,72 +313,65 @@ void MainComponent::openWelcomeWindow() }); } -// Bounds accessors for tutorial steps +// Accessor methods for WelcomeWindow tutorial +std::shared_ptr MainComponent::getModel() { return mainModelTab.getModel(); } + +ChangeBroadcaster& MainComponent::getLoadBroadcaster() { return mainModelTab.getLoadBroadcaster(); } + +// Bounds accessors for tutorial steps - delegate to mainModelTab Rectangle MainComponent::getModelSelectBounds() { - // Combine Combobox and Load Button (Row 1) - if (modelPathComboBox.isVisible()) - { - auto bounds = modelPathComboBox.getBounds(); - bounds = bounds.getUnion(loadModelButton.getBounds()); - return bounds.expanded(2, 2); - } - return {}; + auto bounds = mainModelTab.getModelSelectBounds(); + return getLocalArea(&mainModelTab, bounds); } Rectangle MainComponent::getLoadButtonBounds() { - if (loadModelButton.isVisible()) - return loadModelButton.getBounds().expanded(5, 5); - return {}; + // Load button is part of model selection widget + return getModelSelectBounds(); } Rectangle MainComponent::getControlsBounds() { - if (controlAreaWidget.isVisible()) - return controlAreaWidget.getBounds().expanded(5, 5); - if (controlAreaWidget.isVisible()) - return controlAreaWidget.getBounds().expanded(5, 5); - return {}; + auto bounds = mainModelTab.getControlsBounds(); + return getLocalArea(&mainModelTab, bounds); } Rectangle MainComponent::getInputFolderBounds() { - auto bounds = inputTrackAreaWidget.getFirstTrackFolderButtonBounds(); - return getLocalArea(&inputTrackAreaWidget, bounds); + auto bounds = mainModelTab.getInputFolderBounds(); + return getLocalArea(&mainModelTab, bounds); } Rectangle MainComponent::getInputPlayBounds() { - auto bounds = inputTrackAreaWidget.getFirstTrackPlayButtonBounds(); - return getLocalArea(&inputTrackAreaWidget, bounds); + auto bounds = mainModelTab.getInputPlayBounds(); + return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInputTrackBounds() { return inputTrackAreaWidget.getBounds(); } +Rectangle MainComponent::getInputTrackBounds() +{ + auto bounds = mainModelTab.getInputTrackBounds(); + return getLocalArea(&mainModelTab, bounds); +} -Rectangle MainComponent::getProcessButtonBounds() { return processCancelButton.getBounds(); } +Rectangle MainComponent::getProcessButtonBounds() +{ + auto bounds = mainModelTab.getProcessButtonBounds(); + return getLocalArea(&mainModelTab, bounds); +} Rectangle MainComponent::getTracksBounds() { - auto bounds = inputTrackAreaWidget.getBounds(); - if (outputTrackAreaWidget.isVisible()) - bounds = bounds.getUnion(outputTrackAreaWidget.getBounds()); - - // Include labels if visible - if (inputTracksLabel.isVisible()) - bounds = bounds.getUnion(inputTracksLabel.getBounds()); - if (outputTracksLabel.isVisible()) - bounds = bounds.getUnion(outputTracksLabel.getBounds()); - - return bounds.expanded(5, 5); + auto bounds = mainModelTab.getTracksBounds(); + return getLocalArea(&mainModelTab, bounds); } Rectangle MainComponent::getInfoBarBounds() { - auto bounds = instructionBox->getBounds(); - if (statusBox->isVisible()) - bounds = bounds.getUnion(statusBox->getBounds()); - return bounds.expanded(5, 5); + if (showStatusArea && statusAreaWidget.isVisible()) + return statusAreaWidget.getBounds().expanded(5, 5); + return {}; } Rectangle MainComponent::getClipboardBounds() diff --git a/src/MainComponent.h b/src/MainComponent.h index fd2aa43b..3444420f 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -16,7 +16,6 @@ #include "widgets/StatusAreaWidget.h" #include "windows/AboutWindow.h" -#include "windows/WelcomeWindow.h" #include "windows/settings/SettingsWindow.h" #include "utils/Interface.h" @@ -25,6 +24,9 @@ using namespace juce; +// Forward declaration (include in .cpp) +class WelcomeWindow; + class MainComponent : public Component, public MenuBarModel, public ApplicationCommandTarget { @@ -70,6 +72,22 @@ class MainComponent : public Component, public MenuBarModel, public ApplicationC void setTutorialExtraHighlights(std::vector> bounds); void openWelcomeWindow(); + // Accessor methods for WelcomeWindow tutorial + std::shared_ptr getModel(); + ChangeBroadcaster& getLoadBroadcaster(); + + // Bounds accessors for tutorial steps (public for WelcomeWindow) + Rectangle getModelSelectBounds(); + Rectangle getLoadButtonBounds(); + Rectangle getControlsBounds(); + Rectangle getInputFolderBounds(); + Rectangle getInputPlayBounds(); + Rectangle getInputTrackBounds(); + Rectangle getProcessButtonBounds(); + Rectangle getTracksBounds(); + Rectangle getInfoBarBounds(); + Rectangle getClipboardBounds(); + /* Component */ void paint(Graphics& g) override; @@ -96,18 +114,6 @@ class MainComponent : public Component, public MenuBarModel, public ApplicationC /* Interface */ - // Bounds accessors for tutorial steps - Rectangle getModelSelectBounds(); - Rectangle getLoadButtonBounds(); - Rectangle getControlsBounds(); - Rectangle getInputFolderBounds(); - Rectangle getInputPlayBounds(); - Rectangle getInputTrackBounds(); - Rectangle getProcessButtonBounds(); - Rectangle getTracksBounds(); - Rectangle getInfoBarBounds(); - Rectangle getClipboardBounds(); - bool showStatusArea; bool showMediaClipboard; diff --git a/src/ModelTab.h b/src/ModelTab.h index f8ca079c..bb545ac3 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -48,6 +48,51 @@ class ModelTab : public Component, private ChangeListener ~ModelTab() { modelSelectionWidget.removeChangeListener(this); } + // Accessor methods for WelcomeWindow tutorial + std::shared_ptr getModel() const { return model; } + ChangeBroadcaster& getLoadBroadcaster() { return modelSelectionWidget; } + + // Bounds accessors for tutorial steps + Rectangle getModelSelectBounds() const + { + return modelSelectionWidget.getBounds().expanded(2, 2); + } + + Rectangle getControlsBounds() const + { + if (controlAreaWidget.isVisible()) + return controlAreaWidget.getBounds().expanded(5, 5); + return {}; + } + + Rectangle getInputFolderBounds() + { + return inputTrackAreaWidget.getFirstTrackFolderButtonBounds(); + } + + Rectangle getInputPlayBounds() + { + return inputTrackAreaWidget.getFirstTrackPlayButtonBounds(); + } + + Rectangle getInputTrackBounds() const { return inputTrackAreaWidget.getBounds(); } + + Rectangle getProcessButtonBounds() const { return processCancelButton.getBounds(); } + + Rectangle getTracksBounds() const + { + auto bounds = inputTrackAreaWidget.getBounds(); + if (outputTrackAreaWidget.isVisible()) + bounds = bounds.getUnion(outputTrackAreaWidget.getBounds()); + + if (inputTracksLabel.isVisible()) + bounds = bounds.getUnion(inputTracksLabel.getBounds()); + if (outputTracksLabel.isVisible()) + bounds = bounds.getUnion(outputTracksLabel.getBounds()); + + return bounds.expanded(5, 5); + } + void resized() override { FlexBox tabArea; diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 61e25d74..6260f6a2 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -15,6 +15,9 @@ using namespace juce; +// Forward declaration for TutorialStep +class MainComponent; + struct TutorialStep { String title; @@ -174,9 +177,9 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { auto model = mainComponent->getModel(); - if (model && model->ready()) + if (model && model->isLoaded()) { - modelName = model->card().name; + modelName = model->getMetadata().name; modelId = modelName; // Use full name for matching } } @@ -219,7 +222,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { auto model = mainComponent->getModel(); - if (model && model->ready()) + if (model && model->isLoaded()) { String stepTitle = "Configure Parameters (Optional)"; String baseText = @@ -233,7 +236,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { fullText += "--------------------------------------------------\nDetailed Control Descriptions:\n\n"; - auto& controls = model->getControlsInfo(); + auto controls = model->getControls(); if (controls.empty()) { fullText += "- No adjustable controls for this model."; @@ -241,30 +244,29 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener else { int index = 1; + String modelNameForCtrl = model->getMetadata().name; // First pass: All except Model - for (const auto& pair : controls) + for (const auto& info : controls) { - if (String(pair.second->label).equalsIgnoreCase("Model")) + if (String(info->label).equalsIgnoreCase("Model")) continue; - auto info = pair.second; String friendlyCtrlDesc = getFriendlyControlDescription( - model->card().name, info->label, info->info); + modelNameForCtrl, info->label, info->info); fullText += String(index++) + ". " + info->label + ": " + friendlyCtrlDesc + "\n\n"; } // Second pass: Only Model - for (const auto& pair : controls) + for (const auto& info : controls) { - if (! String(pair.second->label).equalsIgnoreCase("Model")) + if (! String(info->label).equalsIgnoreCase("Model")) continue; - auto info = pair.second; String friendlyCtrlDesc = getFriendlyControlDescription( - model->card().name, info->label, info->info); + modelNameForCtrl, info->label, info->info); fullText += String(index++) + ". " + info->label + ": " + friendlyCtrlDesc + "\n\n"; @@ -653,8 +655,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (dontShowAgainToggle.getToggleState()) { - AppSettings::setValue("showWelcomePopup", 0); - AppSettings::saveIfNeeded(); + Settings::setValue("showWelcomePopup", 0); + Settings::saveIfNeeded(); } closeButtonPressed(); From d5ba58885a154034402957abc62db82c176df309 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Fri, 6 Feb 2026 23:45:28 -0600 Subject: [PATCH 08/13] Fix welcome tutorial flow after refactor --- src/Main.cpp | 29 ++++++++++++++++++++--------- src/MainComponent.cpp | 20 ++++++++++++++++++++ src/ModelTab.h | 18 +++++++++++++++--- src/widgets/ModelSelectionWidget.h | 15 +++++++++++++++ src/windows/WelcomeWindow.h | 8 +++++--- 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 5b7912c6..72ab3945 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -78,15 +78,26 @@ class GuiAppApplication : public JUCEApplication, public FocusChangeListener bool forceShowWelcome = false; - bool showWelcome = true; - - if (! forceShowWelcome) - { - if (Settings::containsKey("view.showWelcomePopup")) - { - showWelcome = Settings::getIntValue("view.showWelcomePopup", 1) == 1; - } - } + bool showWelcome = true; + + if (! forceShowWelcome) + { + const String canonicalWelcomeKey = "view.showWelcomePopup"; + const String legacyWelcomeKey = "showWelcomePopup"; + + if (! Settings::containsKey(canonicalWelcomeKey) + && Settings::containsKey(legacyWelcomeKey)) + { + // One-time migration from legacy key path. + int legacyValue = Settings::getIntValue(legacyWelcomeKey, 1); + Settings::setValue(canonicalWelcomeKey, legacyValue, true); + } + + if (Settings::containsKey(canonicalWelcomeKey)) + { + showWelcome = Settings::getIntValue(canonicalWelcomeKey, 1) == 1; + } + } if (showWelcome) { diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index f54c021b..f47ca402 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -21,6 +21,11 @@ MainComponent::MainComponent() setSize(800, 2000); statusMessage->setMessage("Welcome to HARP!"); + + if (Settings::getIntValue("view.showWelcomePopup", 1) == 1) + { + mainModelTab.loadDefaultModel(); + } } MainComponent::~MainComponent() { deinitializeMenuBar(); } @@ -112,6 +117,11 @@ void MainComponent::resized() } fullWindow.performLayout(fullArea); + + if (welcomeWindow != nullptr) + { + welcomeWindow->refreshHighlightForCurrentStep(); + } } /* --File-- */ @@ -298,6 +308,16 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound void MainComponent::openWelcomeWindow() { + // When tutorial is manually opened from Help and no model is loaded, + // load the default Demucs model so Quick Start text/highlights stay accurate. + mainModelTab.loadDefaultModel(); + + if (welcomeWindow != nullptr) + { + welcomeWindow->toFront(true); + return; + } + MessageManager::callAsync( [this]() { diff --git a/src/ModelTab.h b/src/ModelTab.h index bb545ac3..7f424237 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -50,7 +50,15 @@ class ModelTab : public Component, private ChangeListener // Accessor methods for WelcomeWindow tutorial std::shared_ptr getModel() const { return model; } - ChangeBroadcaster& getLoadBroadcaster() { return modelSelectionWidget; } + ChangeBroadcaster& getLoadBroadcaster() { return modelLoadedBroadcaster; } + + void loadDefaultModel() + { + if (model->isLoaded()) + return; + + modelSelectionWidget.loadModelByPath("teamup-tech/demucs-source-separation"); + } // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const @@ -67,12 +75,14 @@ class ModelTab : public Component, private ChangeListener Rectangle getInputFolderBounds() { - return inputTrackAreaWidget.getFirstTrackFolderButtonBounds(); + auto bounds = inputTrackAreaWidget.getFirstTrackFolderButtonBounds(); + return getLocalArea(&inputTrackAreaWidget, bounds); } Rectangle getInputPlayBounds() { - return inputTrackAreaWidget.getFirstTrackPlayButtonBounds(); + auto bounds = inputTrackAreaWidget.getFirstTrackPlayButtonBounds(); + return getLocalArea(&inputTrackAreaWidget, bounds); } Rectangle getInputTrackBounds() const { return inputTrackAreaWidget.getBounds(); } @@ -336,6 +346,7 @@ class ModelTab : public Component, private ChangeListener outputTrackAreaWidget.updateTracks(model->getOutputTracks()); resized(); + modelLoadedBroadcaster.sendChangeMessage(); // Re-enable processing immediately processCancelButton.setEnabled(true); @@ -491,6 +502,7 @@ class ModelTab : public Component, private ChangeListener ThreadPool loadingThreadPool { 1 }; ThreadPool processingThreadPool { 10 }; + ChangeBroadcaster modelLoadedBroadcaster; std::atomic currentProcessID { 0 }; }; diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index b657b489..9d3aebdf 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -210,6 +210,21 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public String getCurrentlySelectedPath() { return selectedPath; } + bool loadModelByPath(const String& modelPath) + { + for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) + { + if (modelPathComboBox.getItemText(i) == modelPath) + { + modelPathComboBox.setSelectedId(i + 1); + loadModelButton.triggerClick(); + return true; + } + } + + return false; + } + void resetState() { lastLoadedPathIndex = -1; diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 6260f6a2..2c6f4ee8 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -146,10 +146,13 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { currentStep = 2; } - updateStep(); } + + updateStep(); } + void refreshHighlightForCurrentStep() { updateStep(); } + void rebuildSteps() { steps.clear(); @@ -655,8 +658,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (dontShowAgainToggle.getToggleState()) { - Settings::setValue("showWelcomePopup", 0); - Settings::saveIfNeeded(); + Settings::setValue("view.showWelcomePopup", 0, true); } closeButtonPressed(); From e35be81e09c931dc7ea2c76127f7c46df95aea32 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Sat, 7 Feb 2026 10:37:50 -0500 Subject: [PATCH 09/13] Some minor cleanup. --- src/Main.cpp | 31 ++++++++++---------------- src/MainComponent.cpp | 35 ++++++++++++++---------------- src/MainComponent.h | 5 ++--- src/ModelTab.h | 10 +++++---- src/widgets/ModelSelectionWidget.h | 18 ++++----------- src/windows/WelcomeWindow.h | 12 +++++----- 6 files changed, 45 insertions(+), 66 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 72ab3945..c829197d 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -78,26 +78,17 @@ class GuiAppApplication : public JUCEApplication, public FocusChangeListener bool forceShowWelcome = false; - bool showWelcome = true; - - if (! forceShowWelcome) - { - const String canonicalWelcomeKey = "view.showWelcomePopup"; - const String legacyWelcomeKey = "showWelcomePopup"; - - if (! Settings::containsKey(canonicalWelcomeKey) - && Settings::containsKey(legacyWelcomeKey)) - { - // One-time migration from legacy key path. - int legacyValue = Settings::getIntValue(legacyWelcomeKey, 1); - Settings::setValue(canonicalWelcomeKey, legacyValue, true); - } - - if (Settings::containsKey(canonicalWelcomeKey)) - { - showWelcome = Settings::getIntValue(canonicalWelcomeKey, 1) == 1; - } - } + bool showWelcome = true; + + if (! forceShowWelcome) + { + const String welcomeKey = "view.showWelcomePopup"; + + if (Settings::containsKey(welcomeKey)) + { + showWelcome = Settings::getIntValue(welcomeKey, 1); + } + } if (showWelcome) { diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index f47ca402..5fd50afe 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -1,4 +1,5 @@ #include "MainComponent.h" + #include "windows/WelcomeWindow.h" JUCE_IMPLEMENT_SINGLETON(HARPLogger) @@ -22,9 +23,10 @@ MainComponent::MainComponent() statusMessage->setMessage("Welcome to HARP!"); - if (Settings::getIntValue("view.showWelcomePopup", 1) == 1) + if (Settings::getIntValue("view.showWelcomePopup", 1)) { - mainModelTab.loadDefaultModel(); + if (! mainModelTab.isModelLoaded()) + mainModelTab.loadDefaultModel(); } } @@ -310,7 +312,8 @@ void MainComponent::openWelcomeWindow() { // When tutorial is manually opened from Help and no model is loaded, // load the default Demucs model so Quick Start text/highlights stay accurate. - mainModelTab.loadDefaultModel(); + if (! mainModelTab.isModelLoaded()) + mainModelTab.loadDefaultModel(); if (welcomeWindow != nullptr) { @@ -345,10 +348,11 @@ Rectangle MainComponent::getModelSelectBounds() return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getLoadButtonBounds() +Rectangle MainComponent::getInfoBarBounds() { - // Load button is part of model selection widget - return getModelSelectBounds(); + if (showStatusArea && statusAreaWidget.isVisible()) + return statusAreaWidget.getBounds().expanded(5, 5); + return {}; } Rectangle MainComponent::getControlsBounds() @@ -357,21 +361,21 @@ Rectangle MainComponent::getControlsBounds() return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInputFolderBounds() +Rectangle MainComponent::getInputTrackBounds() { - auto bounds = mainModelTab.getInputFolderBounds(); + auto bounds = mainModelTab.getInputTrackBounds(); return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInputPlayBounds() +Rectangle MainComponent::getInputFolderBounds() { - auto bounds = mainModelTab.getInputPlayBounds(); + auto bounds = mainModelTab.getInputFolderBounds(); return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInputTrackBounds() +Rectangle MainComponent::getInputPlayBounds() { - auto bounds = mainModelTab.getInputTrackBounds(); + auto bounds = mainModelTab.getInputPlayBounds(); return getLocalArea(&mainModelTab, bounds); } @@ -387,13 +391,6 @@ Rectangle MainComponent::getTracksBounds() return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInfoBarBounds() -{ - if (showStatusArea && statusAreaWidget.isVisible()) - return statusAreaWidget.getBounds().expanded(5, 5); - return {}; -} - Rectangle MainComponent::getClipboardBounds() { if (showMediaClipboard && mediaClipboardWidget.isVisible()) diff --git a/src/MainComponent.h b/src/MainComponent.h index 3444420f..d530e61c 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -78,14 +78,13 @@ class MainComponent : public Component, public MenuBarModel, public ApplicationC // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); - Rectangle getLoadButtonBounds(); + Rectangle getInfoBarBounds(); Rectangle getControlsBounds(); + Rectangle getInputTrackBounds(); Rectangle getInputFolderBounds(); Rectangle getInputPlayBounds(); - Rectangle getInputTrackBounds(); Rectangle getProcessButtonBounds(); Rectangle getTracksBounds(); - Rectangle getInfoBarBounds(); Rectangle getClipboardBounds(); /* Component */ diff --git a/src/ModelTab.h b/src/ModelTab.h index 7f424237..327e323d 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -54,10 +54,7 @@ class ModelTab : public Component, private ChangeListener void loadDefaultModel() { - if (model->isLoaded()) - return; - - modelSelectionWidget.loadModelByPath("teamup-tech/demucs-source-separation"); + modelSelectionWidget.loadModelBypass("teamup-tech/demucs-source-separation"); } // Bounds accessors for tutorial steps @@ -103,6 +100,11 @@ class ModelTab : public Component, private ChangeListener return bounds.expanded(5, 5); } + bool isModelLoaded() + { + return model->isLoaded(); + } + void resized() override { FlexBox tabArea; diff --git a/src/widgets/ModelSelectionWidget.h b/src/widgets/ModelSelectionWidget.h index 9d3aebdf..bba39217 100644 --- a/src/widgets/ModelSelectionWidget.h +++ b/src/widgets/ModelSelectionWidget.h @@ -210,19 +210,10 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public String getCurrentlySelectedPath() { return selectedPath; } - bool loadModelByPath(const String& modelPath) + void loadModelBypass(const String& modelPath) { - for (int i = 0; i < modelPathComboBox.getNumItems(); ++i) - { - if (modelPathComboBox.getItemText(i) == modelPath) - { - modelPathComboBox.setSelectedId(i + 1); - loadModelButton.triggerClick(); - return true; - } - } - - return false; + selectedPath = modelPath; + sendChangeMessage(); } void resetState() @@ -498,8 +489,7 @@ class ModelSelectionWidget : public Component, public ChangeBroadcaster, public DBG_AND_LOG("ModelSelectionWidget::openCustomPathPopup::loadCallback: " << "Custom path \"" << path << "\" entered."); - selectedPath = path; - sendChangeMessage(); + loadModelBypass(path); }; std::function cancelCallback = [this]() diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 2c6f4ee8..c25206d2 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -11,6 +11,7 @@ #include +#include "../utils/Logging.h" #include "../utils/Settings.h" using namespace juce; @@ -78,7 +79,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // endpointLabel removed addAndMakeVisible(©rightLabel); - copyrightLabel.setText("Copyright 2025 TEAMuP. All rights reserved.", dontSendNotification); + copyrightLabel.setText("Copyright 2026 TEAMuP. All rights reserved.", dontSendNotification); copyrightLabel.setJustificationType(Justification::centred); copyrightLabel.setFont(Font(12.0f)); copyrightLabel.setColour(Label::textColourId, Colours::grey); @@ -87,7 +88,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener nextButton.setButtonText("Next"); nextButton.onClick = [this] { - DBG("Next Button Clicked"); + DBG_AND_LOG("WelcomeWindow::WelcomeWindow: Next button clicked."); nextStep(); }; @@ -220,7 +221,6 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener return v; } }); - // 4. Configure Parameters (Dynamic) // 4. Configure Parameters (Dynamic) if (mainComponent) { @@ -279,9 +279,9 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } - steps.push_back({ stepTitle, fullText, [](MainComponent* c) { - return c->getControlsBounds(); - } }); + steps.push_back({ stepTitle, + fullText, + [](MainComponent* c) { return c->getControlsBounds(); } }); } } From cca8d559a542d66aeb554096b6f58f0f48777adc Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Mon, 9 Feb 2026 01:41:20 -0600 Subject: [PATCH 10/13] review changes --- src/Main.cpp | 16 +-- src/MainComponent.cpp | 109 ++++++++++++-- src/MainComponent.h | 24 +++- src/ModelTab.h | 9 +- src/widgets/MediaClipboardWidget.h | 34 +++++ src/windows/WelcomeWindow.h | 223 +++++++++++++++++++---------- 6 files changed, 318 insertions(+), 97 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index c829197d..63dd8ff1 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -90,14 +90,14 @@ class GuiAppApplication : public JUCEApplication, public FocusChangeListener } } - if (showWelcome) - { - if (auto* mainComp = - dynamic_cast(getMainWindowPtr()->getContentComponent())) - { - mainComp->openWelcomeWindow(); - } - } + if (showWelcome) + { + if (auto* mainComp = + dynamic_cast(getMainWindowPtr()->getContentComponent())) + { + mainComp->openWelcomeWindow(); + } + } StringArray args; diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 5fd50afe..418488b3 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -23,11 +23,6 @@ MainComponent::MainComponent() statusMessage->setMessage("Welcome to HARP!"); - if (Settings::getIntValue("view.showWelcomePopup", 1)) - { - if (! mainModelTab.isModelLoaded()) - mainModelTab.loadDefaultModel(); - } } MainComponent::~MainComponent() { deinitializeMenuBar(); } @@ -308,12 +303,30 @@ void MainComponent::setTutorialExtraHighlights(std::vector> bound repaint(); } -void MainComponent::openWelcomeWindow() +void MainComponent::ensureTutorialModelLoaded() { - // When tutorial is manually opened from Help and no model is loaded, - // load the default Demucs model so Quick Start text/highlights stay accurate. if (! mainModelTab.isModelLoaded()) mainModelTab.loadDefaultModel(); +} + +void MainComponent::resetTutorialAutoLoadedModel() +{ + constexpr const char* tutorialFallbackModelPath = "teamup-tech/demucs-source-separation"; + + if (! mainModelTab.isModelLoaded()) + return; + + if (mainModelTab.getLoadedPath() == tutorialFallbackModelPath) + { + mainModelTab.clearLoadedModel(); + resized(); + } +} + +void MainComponent::openWelcomeWindow(bool ensureDefaultModelLoaded) +{ + if (ensureDefaultModelLoaded) + ensureTutorialModelLoaded(); if (welcomeWindow != nullptr) { @@ -398,6 +411,86 @@ Rectangle MainComponent::getClipboardBounds() return {}; } +Rectangle MainComponent::getClipboardTrackAreaBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getClipboardTrackAreaBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardControlsBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getClipboardControlsBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardNameBoxBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getClipboardNameBoxBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardButtonsBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getClipboardButtonsBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardAddButtonBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getAddFileButtonBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardRemoveButtonBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getRemoveButtonBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardPlayButtonBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getPlayButtonBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + +Rectangle MainComponent::getClipboardSendToDAWButtonBounds() +{ + if (showMediaClipboard && mediaClipboardWidget.isVisible()) + { + auto bounds = mediaClipboardWidget.getSendToDAWButtonBounds(); + return getLocalArea(&mediaClipboardWidget, bounds); + } + return {}; +} + /* --Miscellaneous-- */ // TODO - The following is an old callback from V2. It may be helpful in the future. diff --git a/src/MainComponent.h b/src/MainComponent.h index d530e61c..df6c6d74 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -67,10 +67,12 @@ class MainComponent : public Component, public MenuBarModel, public ApplicationC // Help void openAboutWindow(); - void setTutorialActive(bool active); - void setTutorialHighlight(Rectangle bounds); - void setTutorialExtraHighlights(std::vector> bounds); - void openWelcomeWindow(); + void setTutorialActive(bool active); + void setTutorialHighlight(Rectangle bounds); + void setTutorialExtraHighlights(std::vector> bounds); + void ensureTutorialModelLoaded(); + void resetTutorialAutoLoadedModel(); + void openWelcomeWindow(bool ensureDefaultModelLoaded = false); // Accessor methods for WelcomeWindow tutorial std::shared_ptr getModel(); @@ -83,9 +85,17 @@ class MainComponent : public Component, public MenuBarModel, public ApplicationC Rectangle getInputTrackBounds(); Rectangle getInputFolderBounds(); Rectangle getInputPlayBounds(); - Rectangle getProcessButtonBounds(); - Rectangle getTracksBounds(); - Rectangle getClipboardBounds(); + Rectangle getProcessButtonBounds(); + Rectangle getTracksBounds(); + Rectangle getClipboardBounds(); + Rectangle getClipboardTrackAreaBounds(); + Rectangle getClipboardControlsBounds(); + Rectangle getClipboardNameBoxBounds(); + Rectangle getClipboardButtonsBounds(); + Rectangle getClipboardAddButtonBounds(); + Rectangle getClipboardRemoveButtonBounds(); + Rectangle getClipboardPlayButtonBounds(); + Rectangle getClipboardSendToDAWButtonBounds(); /* Component */ diff --git a/src/ModelTab.h b/src/ModelTab.h index 327e323d..25f4d668 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -51,12 +51,19 @@ class ModelTab : public Component, private ChangeListener // Accessor methods for WelcomeWindow tutorial std::shared_ptr getModel() const { return model; } ChangeBroadcaster& getLoadBroadcaster() { return modelLoadedBroadcaster; } + String getLoadedPath() const { return model->getLoadedPath(); } void loadDefaultModel() { modelSelectionWidget.loadModelBypass("teamup-tech/demucs-source-separation"); } + void clearLoadedModel() + { + resetState(); + resized(); + } + // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const { @@ -192,7 +199,7 @@ class ModelTab : public Component, private ChangeListener void resetState() { - model.reset(); + model = std::make_shared(); modelSelectionWidget.resetState(); modelInfoWidget.resetState(); diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 26266987..3753cbfd 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -131,6 +131,40 @@ class MediaClipboardWidget : public Component, public ChangeListener trackAreaWidget.addTrackFromFilePath(filePath, fromDAW); } + Rectangle getClipboardTrackAreaBounds() const { return trackArea.getBounds().expanded(2); } + + Rectangle getClipboardControlsBounds() const { return controlsComponent.getBounds().expanded(2); } + + Rectangle getClipboardNameBoxBounds() const + { + return getLocalArea(&controlsComponent, selectionTextBox.getBounds()).expanded(2); + } + + Rectangle getClipboardButtonsBounds() const + { + return getLocalArea(&controlsComponent, buttonsComponent.getBounds()).expanded(2); + } + + Rectangle getAddFileButtonBounds() const + { + return getLocalArea(&buttonsComponent, addFileButton.getBounds()).expanded(2); + } + + Rectangle getRemoveButtonBounds() const + { + return getLocalArea(&buttonsComponent, removeSelectionButton.getBounds()).expanded(2); + } + + Rectangle getPlayButtonBounds() const + { + return getLocalArea(&buttonsComponent, playStopButton.getBounds()).expanded(2); + } + + Rectangle getSendToDAWButtonBounds() const + { + return getLocalArea(&buttonsComponent, sendToDAWButton.getBounds()).expanded(2); + } + void addFileCallback() { StringArray validExtensions = MediaDisplayComponent::getSupportedExtensions(); diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index c25206d2..66dcb7d4 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -11,7 +11,6 @@ #include -#include "../utils/Logging.h" #include "../utils/Settings.h" using namespace juce; @@ -41,6 +40,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener setResizable(true, true); setSize(500, 420); setAlwaysOnTop(true); + setContentOwned(new Component(), true); if (mainComponent) { @@ -52,11 +52,11 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener rebuildSteps(); // UI Init - addAndMakeVisible(&titleLabel); + addToContent(titleLabel); titleLabel.setFont(Font(24.0f, Font::bold)); titleLabel.setJustificationType(Justification::centred); - addAndMakeVisible(&descriptionEditor); + addToContent(descriptionEditor); descriptionEditor.setMultiLine(true); descriptionEditor.setReadOnly(true); descriptionEditor.setScrollbarsShown(true); @@ -66,10 +66,9 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener descriptionEditor.setColour(TextEditor::outlineColourId, Colours::transparentBlack); descriptionEditor.setFont(Font(16.0f)); - addAndMakeVisible(&learnMoreLink); + addToContent(learnMoreLink); learnMoreLink.setButtonText("Learn more"); learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); - learnMoreLink.setColour(HyperlinkButton::textColourId, Colours::skyblue); // pyHarpLink init removed @@ -78,38 +77,34 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // endpointLabel removed - addAndMakeVisible(©rightLabel); + addToContent(copyrightLabel); copyrightLabel.setText("Copyright 2026 TEAMuP. All rights reserved.", dontSendNotification); copyrightLabel.setJustificationType(Justification::centred); copyrightLabel.setFont(Font(12.0f)); copyrightLabel.setColour(Label::textColourId, Colours::grey); - addAndMakeVisible(&nextButton); + addToContent(nextButton); nextButton.setButtonText("Next"); - nextButton.onClick = [this] - { - DBG_AND_LOG("WelcomeWindow::WelcomeWindow: Next button clicked."); - nextStep(); - }; + nextButton.onClick = [this] { nextStep(); }; - addAndMakeVisible(&prevButton); + addToContent(prevButton); prevButton.setButtonText("Back"); prevButton.onClick = [this] { prevStep(); }; - addAndMakeVisible(&skipButton); + addToContent(skipButton); skipButton.setButtonText("Skip Tutorial"); skipButton.onClick = [this] { skipTutorial(); }; - addAndMakeVisible(&dontShowAgainToggle); - dontShowAgainToggle.setButtonText("Don't show this again"); + addToContent(dontShowAgainToggle); + dontShowAgainToggle.setButtonText("Dont show again"); dontShowAgainToggle.setToggleState(false, dontSendNotification); - addAndMakeVisible(&pageIndicator); - pageIndicator.setJustificationType(Justification::centred); + addToContent(pageIndicator); + pageIndicator.setJustificationType(Justification::centredLeft); pageIndicator.setFont(Font(12.0f)); pageIndicator.setInterceptsMouseClicks(false, false); - addAndMakeVisible(&showDetailsButton); + addToContent(showDetailsButton); showDetailsButton.setButtonText("Show detailed control descriptions"); showDetailsButton.onClick = [this] { @@ -137,6 +132,26 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener void changeListenerCallback(ChangeBroadcaster*) override { // Model loaded/changed + if (pendingTutorialFallbackLoad) + { + if (mainComponent != nullptr) + { + auto model = mainComponent->getModel(); + auto loadedPath = model ? model->getLoadedPath() : String(); + + autoLoadedByTutorialFallback = + (loadedPath == "teamup-tech/demucs-source-separation"); + } + pendingTutorialFallbackLoad = false; + } + else if (autoLoadedByTutorialFallback && mainComponent != nullptr) + { + auto model = mainComponent->getModel(); + auto loadedPath = model ? model->getLoadedPath() : String(); + if (loadedPath != "teamup-tech/demucs-source-separation") + autoLoadedByTutorialFallback = false; + } + rebuildSteps(); if (currentStep > 0) @@ -171,12 +186,10 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { "Select a Model", "Select a model from the dropdown menu at the top and click Load.\n\n" "Once loaded, the model's details and controls will appear below.\n\n" - "If you don't select another model, HARP uses the Demucs Stem Separator by default.", + "If you click Next without loading a model, HARP loads Demucs for guidance.", [](MainComponent* c) { return c->getModelSelectBounds(); } }); - // 3. Model Overview (Dynamic) - String modelName = "Demucs Source Separation"; // Default - String modelId = "demucs"; + String modelName = "current model"; if (mainComponent) { @@ -184,7 +197,6 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (model && model->isLoaded()) { modelName = model->getMetadata().name; - modelId = modelName; // Use full name for matching } } @@ -227,7 +239,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener auto model = mainComponent->getModel(); if (model && model->isLoaded()) { - String stepTitle = "Configure Parameters (Optional)"; + String controlsStepTitle = "Configure Parameters (Optional)"; String baseText = "Some models will have controls to customize model behavior. Typical controls include ones that balance quality vs speed, allow selection of model variants, or provide advanced options for experienced users.\n\n" "To see descriptions of what controls do for a model Click on the button below. Do this now to see the control descriptions for " @@ -279,7 +291,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } - steps.push_back({ stepTitle, + steps.push_back({ controlsStepTitle, fullText, [](MainComponent* c) { return c->getControlsBounds(); } }); } @@ -290,7 +302,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { "Manage Tracks", "The highlighted panel contains your input and output tracks.\n\n" "You can drag and drop audio/MIDI files here, or click on the folder icon in the input audio section to choose from a local folder.\n\n" - "Processed results will appear in the output tracks section, which you can play here or save directly.\n\n" + "Processed results appear in the output tracks section.\n\n" "Please note that these tracks interact with the model.", [](MainComponent* c) { return c->getTracksBounds(); }, nullptr }); @@ -306,11 +318,16 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener steps.push_back( { "Media Clipboard", - "This clipboard is a scratch space. Tracks here do not interact with the models directly.\n\n" - "However, you can stash model outputs here to reuse them later.\n\n" - "To stash the input or output track in the clipboard, drag the track here to re-use it later.", + "This clipboard is a scratch space and does not feed model inputs directly.\n\n" + "You can add tracks with the folder icon in the clipboard toolbar, rename a selected track in the text box above the list, remove, play, or save a selected entry, and send selected tracks to your DAW with the send icon.\n\n" + "Use it to stash useful outputs and reuse them across model runs.", [](MainComponent* c) { return c->getClipboardBounds(); }, - nullptr }); + [](MainComponent* c) + { + std::vector> v; + v.push_back(c->getClipboardControlsBounds()); + return v; + } }); // 8. Interface Summary steps.push_back( @@ -318,7 +335,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener "Top Bar: Select a model from the dropdown and click Load to initialize it.\n\n" "Left Panel: This is where your Input and Output tracks live. Models read from and write to these tracks.\n\n" "Right Panel: A scratch pad (Clipboard) to stash tracks you want to save or reuse later.", - [](MainComponent* c) { return Rectangle(); }, + [](MainComponent*) { return Rectangle(); }, [](MainComponent* c) { std::vector> v; @@ -522,53 +539,97 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener void resized() override { - auto area = getLocalBounds().reduced(20); - - // Header - titleLabel.setBounds(area.removeFromTop(40)); - area.removeFromTop(10); - - // Footer Buttons - auto footer = area.removeFromBottom(30); - auto buttonWidth = 100; - skipButton.setBounds(footer.removeFromLeft(buttonWidth)); - nextButton.setBounds(footer.removeFromRight(buttonWidth)); - footer.removeFromRight(10); - prevButton.setBounds(footer.removeFromRight(buttonWidth)); - pageIndicator.setBounds(footer); + DocumentWindow::resized(); + ensureContentChildrenAttached(); - area.removeFromBottom(10); // Spacer - - if (currentStep == 0) - { - copyrightLabel.setBounds(area.removeFromBottom(20)); - descriptionEditor.setBounds(area.removeFromTop(200)); - auto linkArea = area.removeFromTop(30); - learnMoreLink.setBounds(linkArea.reduced(linkArea.getWidth() / 2 - 50, 0)); - } - else + if (auto* content = resolveContentRoot()) { - if (currentStep == (int) steps.size() - 1) + auto area = content->getLocalBounds().reduced(20); + + titleLabel.setBounds(area.removeFromTop(40)); + area.removeFromTop(10); + + auto footerArea = area.removeFromBottom(72); + auto footer = footerArea.removeFromTop(30); + auto toggleRow = footerArea.removeFromBottom(30); + constexpr int buttonWidth = 100; + skipButton.setBounds(footer.removeFromLeft(buttonWidth)); + nextButton.setBounds(footer.removeFromRight(buttonWidth)); + footer.removeFromRight(10); + prevButton.setBounds(footer.removeFromRight(buttonWidth)); + const int middleRightX = prevButton.isVisible() ? (prevButton.getX() - 10) : nextButton.getX(); + const int middleX = skipButton.getRight() + 12; + const int middleW = jmax(0, middleRightX - middleX); + copyrightLabel.setBounds(middleX, footer.getY(), middleW, footer.getHeight()); + + constexpr int toggleWidth = 170; + constexpr int toggleHeight = 26; + const int toggleY = toggleRow.getY() + 12; + const int toggleX = nextButton.getRight() - toggleWidth; + dontShowAgainToggle.setBounds(toggleX, toggleY, toggleWidth, toggleHeight); + pageIndicator.setBounds(skipButton.getX(), toggleY, skipButton.getWidth(), 26); + + area.removeFromBottom(8); + + if (currentStep == 0) { - auto checkArea = area.removeFromBottom(30); - dontShowAgainToggle.setBounds(checkArea.removeFromRight(200)); + auto learnMoreArea = area.removeFromBottom(36); + learnMoreLink.setBounds(learnMoreArea.withSizeKeepingCentre(180, 30)); + area.removeFromBottom(4); + descriptionEditor.setBounds(area); } - - // Fix for UI overlap: Reserve space for the "show details" button at the bottom FIRST - if (showDetailsButton.isVisible()) + else { - auto btnArea = area.removeFromBottom(30); - showDetailsButton.setBounds(btnArea.reduced(20, 0)); - // Add some spacing between button and description - area.removeFromBottom(10); - } + if (showDetailsButton.isVisible()) + { + auto btnArea = area.removeFromBottom(30); + showDetailsButton.setBounds(btnArea.reduced(20, 0)); + area.removeFromBottom(8); + } - // Finally, the description editor takes the remaining space - descriptionEditor.setBounds(area); + descriptionEditor.setBounds(area); + learnMoreLink.setVisible(false); + } } } private: + void ensureContentChildrenAttached() + { + if (auto* content = resolveContentRoot()) + { + auto attach = [content](Component& component) + { + if (component.getParentComponent() != content) + content->addAndMakeVisible(component); + }; + + attach(titleLabel); + attach(descriptionEditor); + attach(learnMoreLink); + attach(copyrightLabel); + attach(nextButton); + attach(prevButton); + attach(skipButton); + attach(dontShowAgainToggle); + attach(pageIndicator); + attach(showDetailsButton); + } + } + + void addToContent(Component& component) + { + if (auto* content = resolveContentRoot()) + content->addAndMakeVisible(component); + } + + Component* resolveContentRoot() + { + if (contentRoot == nullptr) + contentRoot = getContentComponent(); + return contentRoot; + } + void updateStep() { if (steps.empty()) @@ -604,8 +665,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // bool isSecond = currentStep == 1; // "Select a Model" nextButton.setButtonText(isLast ? "Finish" : "Next"); - skipButton.setVisible(! isLast); - dontShowAgainToggle.setVisible(isLast); + skipButton.setVisible(true); + dontShowAgainToggle.setVisible(true); // Items specific to first page learnMoreLink.setVisible(isFirst); @@ -628,6 +689,17 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener void nextStep() { + if (currentStep == 1 && mainComponent != nullptr) + { + auto model = mainComponent->getModel(); + if (! model || ! model->isLoaded()) + { + pendingTutorialFallbackLoad = true; + mainComponent->ensureTutorialModelLoaded(); + return; + } + } + if (currentStep < (int) steps.size() - 1) { currentStep++; @@ -650,8 +722,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener void skipTutorial() { - currentStep = (int) steps.size() - 1; - updateStep(); + finishTutorial(); } void finishTutorial() @@ -661,12 +732,18 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener Settings::setValue("view.showWelcomePopup", 0, true); } + if (autoLoadedByTutorialFallback && mainComponent != nullptr) + mainComponent->resetTutorialAutoLoadedModel(); + closeButtonPressed(); } MainComponent* mainComponent; + Component* contentRoot = nullptr; std::vector steps; int currentStep = 0; + bool pendingTutorialFallbackLoad = false; + bool autoLoadedByTutorialFallback = false; Label titleLabel; TextEditor descriptionEditor; // Changed from Label From c772b9e6307ecc33728ddad8b1616d1f1df32c38 Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Mon, 9 Feb 2026 02:50:44 -0600 Subject: [PATCH 11/13] cleanup --- src/MainComponent.cpp | 26 +++++++++++++++----------- src/ModelTab.h | 3 ++- src/utils/TutorialConstants.h | 7 +++++++ src/windows/WelcomeWindow.h | 24 ++++++++++++++++++------ 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 src/utils/TutorialConstants.h diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 418488b3..dc2f0cc9 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -1,6 +1,7 @@ #include "MainComponent.h" #include "windows/WelcomeWindow.h" +#include "utils/TutorialConstants.h" JUCE_IMPLEMENT_SINGLETON(HARPLogger) @@ -311,12 +312,10 @@ void MainComponent::ensureTutorialModelLoaded() void MainComponent::resetTutorialAutoLoadedModel() { - constexpr const char* tutorialFallbackModelPath = "teamup-tech/demucs-source-separation"; - if (! mainModelTab.isModelLoaded()) return; - if (mainModelTab.getLoadedPath() == tutorialFallbackModelPath) + if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) { mainModelTab.clearLoadedModel(); resized(); @@ -334,18 +333,23 @@ void MainComponent::openWelcomeWindow(bool ensureDefaultModelLoaded) return; } + Component::SafePointer safeThis(this); MessageManager::callAsync( - [this]() + [safeThis]() { - welcomeWindow.reset(new WelcomeWindow(this)); + if (safeThis == nullptr) + return; - // Handle window closing - welcomeWindow->onClose = [this]() { welcomeWindow.reset(); }; + safeThis->welcomeWindow.reset(new WelcomeWindow(safeThis.getComponent())); + safeThis->welcomeWindow->onClose = [safeThis]() + { + if (safeThis != nullptr) + safeThis->welcomeWindow.reset(); + }; - // Position relative to main window or center - welcomeWindow->centreWithSize(500, 420); - welcomeWindow->setVisible(true); - welcomeWindow->toFront(true); + safeThis->welcomeWindow->centreWithSize(500, 420); + safeThis->welcomeWindow->setVisible(true); + safeThis->welcomeWindow->toFront(true); }); } diff --git a/src/ModelTab.h b/src/ModelTab.h index 25f4d668..a6f46088 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -17,6 +17,7 @@ #include "utils/Errors.h" #include "utils/Logging.h" +#include "utils/TutorialConstants.h" using namespace juce; @@ -55,7 +56,7 @@ class ModelTab : public Component, private ChangeListener void loadDefaultModel() { - modelSelectionWidget.loadModelBypass("teamup-tech/demucs-source-separation"); + modelSelectionWidget.loadModelBypass(TutorialConstants::fallbackModelPath); } void clearLoadedModel() diff --git a/src/utils/TutorialConstants.h b/src/utils/TutorialConstants.h new file mode 100644 index 00000000..63aef526 --- /dev/null +++ b/src/utils/TutorialConstants.h @@ -0,0 +1,7 @@ +#pragma once + +namespace TutorialConstants +{ +inline constexpr const char* fallbackModelPath = "teamup-tech/demucs-source-separation"; +} + diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 66dcb7d4..31c85880 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -12,6 +12,7 @@ #include #include "../utils/Settings.h" +#include "../utils/TutorialConstants.h" using namespace juce; @@ -140,7 +141,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener auto loadedPath = model ? model->getLoadedPath() : String(); autoLoadedByTutorialFallback = - (loadedPath == "teamup-tech/demucs-source-separation"); + (loadedPath == TutorialConstants::fallbackModelPath); } pendingTutorialFallbackLoad = false; } @@ -148,7 +149,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { auto model = mainComponent->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); - if (loadedPath != "teamup-tech/demucs-source-separation") + if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; } @@ -552,22 +553,33 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener auto footerArea = area.removeFromBottom(72); auto footer = footerArea.removeFromTop(30); auto toggleRow = footerArea.removeFromBottom(30); - constexpr int buttonWidth = 100; + + const int buttonHeight = 30; + const int buttonPaddingX = 28; + const int buttonWidth = jmax(skipButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX, + nextButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX); skipButton.setBounds(footer.removeFromLeft(buttonWidth)); nextButton.setBounds(footer.removeFromRight(buttonWidth)); footer.removeFromRight(10); prevButton.setBounds(footer.removeFromRight(buttonWidth)); + const int middleRightX = prevButton.isVisible() ? (prevButton.getX() - 10) : nextButton.getX(); const int middleX = skipButton.getRight() + 12; const int middleW = jmax(0, middleRightX - middleX); copyrightLabel.setBounds(middleX, footer.getY(), middleW, footer.getHeight()); - constexpr int toggleWidth = 170; constexpr int toggleHeight = 26; + const int toggleTextWidth = + Font(16.0f).getStringWidth(dontShowAgainToggle.getButtonText()); + const int toggleWidth = 24 + toggleTextWidth + 16; const int toggleY = toggleRow.getY() + 12; - const int toggleX = nextButton.getRight() - toggleWidth; + const int toggleX = toggleRow.getRight() - toggleWidth; dontShowAgainToggle.setBounds(toggleX, toggleY, toggleWidth, toggleHeight); - pageIndicator.setBounds(skipButton.getX(), toggleY, skipButton.getWidth(), 26); + + const int stepWidth = + jmax(skipButton.getWidth(), + pageIndicator.getFont().getStringWidth(pageIndicator.getText()) + 16); + pageIndicator.setBounds(toggleRow.getX(), toggleY, stepWidth, 26); area.removeFromBottom(8); From 66fd4726f4d9a658caf65a78b0ec601d4f069fe6 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Sat, 14 Feb 2026 08:23:23 -0500 Subject: [PATCH 12/13] Some cleanup and reorganization, removed redundant broadcaster, fixed toggle logic, adjusted bounds, and disable resizing for walkthrough. --- src/Main.cpp | 22 +- src/MainComponent.cpp | 91 +++-- src/MainComponent.h | 6 +- src/ModelTab.h | 25 +- src/utils/{TutorialConstants.h => Tutorial.h} | 7 +- src/windows/WelcomeWindow.h | 322 ++++++++---------- 6 files changed, 212 insertions(+), 261 deletions(-) rename src/utils/{TutorialConstants.h => Tutorial.h} (57%) diff --git a/src/Main.cpp b/src/Main.cpp index c72f3509..961955dc 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -76,29 +76,17 @@ class GuiAppApplication : public JUCEApplication, public FocusChangeListener mainWindow.reset(new HARPWindow(windowTitle)); - bool forceShowWelcome = false; + bool showWelcome = Settings::getBoolValue("view.showWelcomePopup", true); - bool showWelcome = true; - - if (! forceShowWelcome) + if (showWelcome) { - const String welcomeKey = "view.showWelcomePopup"; - - if (Settings::containsKey(welcomeKey)) + if (auto* mainComp = + dynamic_cast(getMainWindowPtr()->getContentComponent())) { - showWelcome = Settings::getIntValue(welcomeKey, 1); + mainComp->openWelcomeWindow(); } } - if (showWelcome) - { - if (auto* mainComp = - dynamic_cast(getMainWindowPtr()->getContentComponent())) - { - mainComp->openWelcomeWindow(); - } - } - StringArray args; // Split command line arguments at spaces diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 5257fee3..d3b4165e 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -1,7 +1,6 @@ #include "MainComponent.h" #include "windows/WelcomeWindow.h" -#include "utils/TutorialConstants.h" JUCE_IMPLEMENT_SINGLETON(HARPLogger) @@ -27,7 +26,6 @@ MainComponent::MainComponent() sharedTokens->initializeAPIKeys(); statusMessage->setMessage("Welcome to HARP!"); - } MainComponent::~MainComponent() @@ -356,6 +354,39 @@ void MainComponent::openAboutWindow() options.launchAsync(); } +void MainComponent::openWelcomeWindow(bool ensureDefaultModelLoaded) +{ + if (ensureDefaultModelLoaded) + ensureTutorialModelLoaded(); + + if (welcomeWindow != nullptr) + { + welcomeWindow->toFront(true); + return; + } + + Component::SafePointer safeThis(this); + MessageManager::callAsync( + [safeThis]() + { + if (safeThis == nullptr) + return; + + safeThis->welcomeWindow.reset(new WelcomeWindow(safeThis.getComponent())); + safeThis->welcomeWindow->onClose = [safeThis]() + { + if (safeThis != nullptr) + safeThis->welcomeWindow.reset(); + }; + + safeThis->welcomeWindow->centreWithSize(500, 420); + safeThis->welcomeWindow->setVisible(true); + safeThis->welcomeWindow->toFront(true); + }); +} + +/* --Tutorial-- */ + void MainComponent::setTutorialActive(bool active) { isTutorialActive = active; @@ -387,61 +418,16 @@ void MainComponent::resetTutorialAutoLoadedModel() if (mainModelTab.getLoadedPath() == TutorialConstants::fallbackModelPath) { - mainModelTab.clearLoadedModel(); - resized(); - } -} - -void MainComponent::openWelcomeWindow(bool ensureDefaultModelLoaded) -{ - if (ensureDefaultModelLoaded) - ensureTutorialModelLoaded(); - - if (welcomeWindow != nullptr) - { - welcomeWindow->toFront(true); - return; + mainModelTab.resetState(); } - - Component::SafePointer safeThis(this); - MessageManager::callAsync( - [safeThis]() - { - if (safeThis == nullptr) - return; - - safeThis->welcomeWindow.reset(new WelcomeWindow(safeThis.getComponent())); - safeThis->welcomeWindow->onClose = [safeThis]() - { - if (safeThis != nullptr) - safeThis->welcomeWindow.reset(); - }; - - safeThis->welcomeWindow->centreWithSize(500, 420); - safeThis->welcomeWindow->setVisible(true); - safeThis->welcomeWindow->toFront(true); - }); } -// Accessor methods for WelcomeWindow tutorial -std::shared_ptr MainComponent::getModel() { return mainModelTab.getModel(); } - -ChangeBroadcaster& MainComponent::getLoadBroadcaster() { return mainModelTab.getLoadBroadcaster(); } - -// Bounds accessors for tutorial steps - delegate to mainModelTab Rectangle MainComponent::getModelSelectBounds() { auto bounds = mainModelTab.getModelSelectBounds(); return getLocalArea(&mainModelTab, bounds); } -Rectangle MainComponent::getInfoBarBounds() -{ - if (showStatusArea && statusAreaWidget.isVisible()) - return statusAreaWidget.getBounds().expanded(5, 5); - return {}; -} - Rectangle MainComponent::getControlsBounds() { auto bounds = mainModelTab.getControlsBounds(); @@ -481,7 +467,7 @@ Rectangle MainComponent::getTracksBounds() Rectangle MainComponent::getClipboardBounds() { if (showMediaClipboard && mediaClipboardWidget.isVisible()) - return mediaClipboardWidget.getBounds().expanded(5, 5); + return mediaClipboardWidget.getBounds(); return {}; } @@ -565,6 +551,13 @@ Rectangle MainComponent::getClipboardSendToDAWButtonBounds() return {}; } +Rectangle MainComponent::getInfoBarBounds() +{ + if (showStatusArea && statusAreaWidget.isVisible()) + return statusAreaWidget.getBounds(); + return {}; +} + /* --Miscellaneous-- */ // TODO - The following is an old callback from V2. It may be helpful in the future. diff --git a/src/MainComponent.h b/src/MainComponent.h index 30981180..2acb5841 100644 --- a/src/MainComponent.h +++ b/src/MainComponent.h @@ -21,6 +21,7 @@ #include "utils/Interface.h" #include "utils/Logging.h" #include "utils/Settings.h" +#include "utils/Tutorial.h" using namespace juce; @@ -73,8 +74,7 @@ class MainComponent : public Component, /* Tutorial */ - std::shared_ptr getModel(); - ChangeBroadcaster& getLoadBroadcaster(); + ModelTab* getModelTab() { return &mainModelTab; } void setTutorialActive(bool active); void setTutorialHighlight(Rectangle bounds); @@ -84,7 +84,6 @@ class MainComponent : public Component, // Bounds accessors for tutorial steps (public for WelcomeWindow) Rectangle getModelSelectBounds(); - Rectangle getInfoBarBounds(); Rectangle getControlsBounds(); Rectangle getInputTrackBounds(); Rectangle getInputFolderBounds(); @@ -100,6 +99,7 @@ class MainComponent : public Component, Rectangle getClipboardRemoveButtonBounds(); Rectangle getClipboardPlayButtonBounds(); Rectangle getClipboardSendToDAWButtonBounds(); + Rectangle getInfoBarBounds(); /* Component */ diff --git a/src/ModelTab.h b/src/ModelTab.h index 17663ae1..e6e8e1d0 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -17,7 +17,7 @@ #include "utils/Errors.h" #include "utils/Logging.h" -#include "utils/TutorialConstants.h" +#include "utils/Tutorial.h" using namespace juce; @@ -51,7 +51,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas // Accessor methods for WelcomeWindow tutorial std::shared_ptr getModel() const { return model; } - ChangeBroadcaster& getLoadBroadcaster() { return modelLoadedBroadcaster; } String getLoadedPath() const { return model->getLoadedPath(); } void loadDefaultModel() @@ -59,12 +58,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas modelSelectionWidget.loadModelBypass(TutorialConstants::fallbackModelPath); } - void clearLoadedModel() - { - resetState(); - resized(); - } - // Bounds accessors for tutorial steps Rectangle getModelSelectBounds() const { @@ -73,8 +66,11 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas Rectangle getControlsBounds() const { - if (controlAreaWidget.isVisible()) - return controlAreaWidget.getBounds().expanded(5, 5); + auto bounds = controlAreaWidget.getBounds(); + + if (bounds.getWidth() > 0 && bounds.getHeight() > 0) + return bounds.expanded(2, 2); + return {}; } @@ -105,7 +101,7 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas if (outputTracksLabel.isVisible()) bounds = bounds.getUnion(outputTracksLabel.getBounds()); - return bounds.expanded(5, 5); + return bounds.expanded(2, 2); } bool isModelLoaded() @@ -247,6 +243,8 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas processCancelButton.setEnabled(false); currentProcessID = 0; + + resized(); } private: @@ -438,9 +436,9 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas inputTrackAreaWidget.updateTracks(model->getInputTracks()); outputTrackAreaWidget.updateTracks(model->getOutputTracks()); - sendSynchronousChangeMessage(); resized(); - modelLoadedBroadcaster.sendChangeMessage(); + + sendSynchronousChangeMessage(); // Re-enable processing immediately processCancelButton.setEnabled(true); @@ -603,7 +601,6 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas ThreadPool loadingThreadPool { 1 }; ThreadPool processingThreadPool { 10 }; - ChangeBroadcaster modelLoadedBroadcaster; std::atomic currentProcessID { 0 }; }; diff --git a/src/utils/TutorialConstants.h b/src/utils/Tutorial.h similarity index 57% rename from src/utils/TutorialConstants.h rename to src/utils/Tutorial.h index 63aef526..835a0593 100644 --- a/src/utils/TutorialConstants.h +++ b/src/utils/Tutorial.h @@ -1,7 +1,12 @@ +/** + * @file Tutorial.h + * @brief Constants needed for tutorial steps. + * @author saumya-pailwan + */ + #pragma once namespace TutorialConstants { inline constexpr const char* fallbackModelPath = "teamup-tech/demucs-source-separation"; } - diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 31c85880..2639dcdf 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -12,7 +12,7 @@ #include #include "../utils/Settings.h" -#include "../utils/TutorialConstants.h" +#include "../utils/Tutorial.h" using namespace juce; @@ -38,94 +38,34 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener mainComponent(mainComp) { setUsingNativeTitleBar(true); - setResizable(true, true); - setSize(500, 420); + WelcomeContent* c = new WelcomeContent(*this); + setContentOwned(c, true); + content = c; + setAlwaysOnTop(true); - setContentOwned(new Component(), true); + setResizable(true, true); // Still need to set this to fix size with constrainer + centreWithSize(content->getWidth(), content->getHeight()); + + setConstrainer(&constrainer); + constrainer.setMinimumSize(content->getWidth(), content->getHeight()); + constrainer.setMaximumSize(content->getWidth(), content->getHeight()); + constrainer.setMinimumOnscreenAmounts(40, 40, 40, 40); if (mainComponent) { - mainComponent->getLoadBroadcaster().addChangeListener(this); + mainComponent->getModelTab()->addChangeListener(this); mainComponent->setTutorialActive(true); } - // Initial Build rebuildSteps(); - - // UI Init - addToContent(titleLabel); - titleLabel.setFont(Font(24.0f, Font::bold)); - titleLabel.setJustificationType(Justification::centred); - - addToContent(descriptionEditor); - descriptionEditor.setMultiLine(true); - descriptionEditor.setReadOnly(true); - descriptionEditor.setScrollbarsShown(true); - descriptionEditor.setCaretVisible(false); - // Make it look like a label (transparent) - descriptionEditor.setColour(TextEditor::backgroundColourId, Colours::transparentBlack); - descriptionEditor.setColour(TextEditor::outlineColourId, Colours::transparentBlack); - descriptionEditor.setFont(Font(16.0f)); - - addToContent(learnMoreLink); - learnMoreLink.setButtonText("Learn more"); - learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); - - // pyHarpLink init removed - - // Step 2 Labels init - // hostingEditor init removed - - // endpointLabel removed - - addToContent(copyrightLabel); - copyrightLabel.setText("Copyright 2026 TEAMuP. All rights reserved.", dontSendNotification); - copyrightLabel.setJustificationType(Justification::centred); - copyrightLabel.setFont(Font(12.0f)); - copyrightLabel.setColour(Label::textColourId, Colours::grey); - - addToContent(nextButton); - nextButton.setButtonText("Next"); - nextButton.onClick = [this] { nextStep(); }; - - addToContent(prevButton); - prevButton.setButtonText("Back"); - prevButton.onClick = [this] { prevStep(); }; - - addToContent(skipButton); - skipButton.setButtonText("Skip Tutorial"); - skipButton.onClick = [this] { skipTutorial(); }; - - addToContent(dontShowAgainToggle); - dontShowAgainToggle.setButtonText("Dont show again"); - dontShowAgainToggle.setToggleState(false, dontSendNotification); - - addToContent(pageIndicator); - pageIndicator.setJustificationType(Justification::centredLeft); - pageIndicator.setFont(Font(12.0f)); - pageIndicator.setInterceptsMouseClicks(false, false); - - addToContent(showDetailsButton); - showDetailsButton.setButtonText("Show detailed control descriptions"); - showDetailsButton.onClick = [this] - { - showingDetails = ! showingDetails; - showDetailsButton.setButtonText(showingDetails ? "Hide detailed control descriptions" - : "Show detailed control descriptions"); - rebuildSteps(); - updateStep(); - }; - - // Initial Update updateStep(); - setVisible(true); } ~WelcomeWindow() override { if (mainComponent) { - mainComponent->getLoadBroadcaster().removeChangeListener(this); + mainComponent->getModelTab()->removeChangeListener(this); mainComponent->setTutorialActive(false); } } @@ -137,17 +77,16 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener { if (mainComponent != nullptr) { - auto model = mainComponent->getModel(); + auto model = mainComponent->getModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); - autoLoadedByTutorialFallback = - (loadedPath == TutorialConstants::fallbackModelPath); + autoLoadedByTutorialFallback = (loadedPath == TutorialConstants::fallbackModelPath); } pendingTutorialFallbackLoad = false; } else if (autoLoadedByTutorialFallback && mainComponent != nullptr) { - auto model = mainComponent->getModel(); + auto model = mainComponent->getModelTab()->getModel(); auto loadedPath = model ? model->getLoadedPath() : String(); if (loadedPath != TutorialConstants::fallbackModelPath) autoLoadedByTutorialFallback = false; @@ -155,13 +94,13 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener rebuildSteps(); - if (currentStep > 0) + if (content->currentStep > 0) { // Auto-jump to Step 3 ("Quick Start") if user loads a new model // (Index 2 corresponds to "Quick Start") if (steps.size() > 2) { - currentStep = 2; + content->currentStep = 2; } } @@ -194,7 +133,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener if (mainComponent) { - auto model = mainComponent->getModel(); + auto model = mainComponent->getModelTab()->getModel(); if (model && model->isLoaded()) { modelName = model->getMetadata().name; @@ -224,7 +163,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener bounds.setRight(clipboard.getX()); } return bounds; - }, // Highlight entire interface except clipboard + }, + // Highlight entire interface except clipboard [](MainComponent* c) { std::vector> v; @@ -237,7 +177,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // 4. Configure Parameters (Dynamic) if (mainComponent) { - auto model = mainComponent->getModel(); + auto model = mainComponent->getModelTab()->getModel(); if (model && model->isLoaded()) { String controlsStepTitle = "Configure Parameters (Optional)"; @@ -248,7 +188,7 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener String fullText = baseText; - if (showingDetails) + if (content->showingDetails) { fullText += "--------------------------------------------------\nDetailed Control Descriptions:\n\n"; @@ -357,8 +297,8 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener // Refresh if we are showing the tutorial if (isVisible()) { - if (currentStep >= (int) steps.size()) - currentStep = (int) steps.size() - 1; + if (content->currentStep >= (int) steps.size()) + content->currentStep = (int) steps.size() - 1; updateStep(); } } @@ -538,14 +478,73 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener g.fillAll(getLookAndFeel().findColour(ResizableWindow::backgroundColourId)); } - void resized() override +private: + class WelcomeContent : public Component { - DocumentWindow::resized(); - ensureContentChildrenAttached(); + public: + explicit WelcomeContent(WelcomeWindow& ownerRef) : owner(ownerRef) + { + // UI Init + addAndMakeVisible(titleLabel); + titleLabel.setFont(Font(24.0f, Font::bold)); + titleLabel.setJustificationType(Justification::centred); + + addAndMakeVisible(descriptionEditor); + descriptionEditor.setMultiLine(true); + descriptionEditor.setReadOnly(true); + descriptionEditor.setScrollbarsShown(true); + descriptionEditor.setCaretVisible(false); + // Make it look like a label (transparent) + descriptionEditor.setColour(TextEditor::backgroundColourId, Colours::transparentBlack); + descriptionEditor.setColour(TextEditor::outlineColourId, Colours::transparentBlack); + descriptionEditor.setFont(Font(16.0f)); + + addAndMakeVisible(learnMoreLink); + learnMoreLink.setButtonText("Learn more"); + learnMoreLink.setURL(URL("https://harp-plugin.netlify.app/content/intro.html")); + + addAndMakeVisible(copyrightLabel); + copyrightLabel.setText("Copyright 2026 TEAMuP. All rights reserved.", + dontSendNotification); + copyrightLabel.setJustificationType(Justification::centred); + copyrightLabel.setFont(Font(12.0f)); + copyrightLabel.setColour(Label::textColourId, Colours::grey); + + addAndMakeVisible(nextButton); + nextButton.onClick = [this] { owner.nextStep(); }; + + addAndMakeVisible(prevButton); + prevButton.onClick = [this] { owner.prevStep(); }; + + addAndMakeVisible(skipButton); + skipButton.onClick = [this] { owner.skipTutorial(); }; + + addAndMakeVisible(dontShowAgainToggle); + bool notToggleState = Settings::getBoolValue("view.showWelcomePopup", true); + dontShowAgainToggle.setToggleState(! notToggleState, dontSendNotification); + + addAndMakeVisible(pageIndicator); + pageIndicator.setJustificationType(Justification::centredLeft); + pageIndicator.setFont(Font(12.0f)); + pageIndicator.setInterceptsMouseClicks(false, false); + + addAndMakeVisible(showDetailsButton); + showDetailsButton.onClick = [this] + { + showingDetails = ! showingDetails; + showDetailsButton.setButtonText(showingDetails + ? "Hide detailed control descriptions" + : "Show detailed control descriptions"); + owner.rebuildSteps(); + owner.updateStep(); + }; + + setSize(500, 420); + } - if (auto* content = resolveContentRoot()) + void resized() override { - auto area = content->getLocalBounds().reduced(20); + auto area = getLocalBounds().reduced(20); titleLabel.setBounds(area.removeFromTop(40)); area.removeFromTop(10); @@ -556,14 +555,16 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener const int buttonHeight = 30; const int buttonPaddingX = 28; - const int buttonWidth = jmax(skipButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX, - nextButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX); + const int buttonWidth = + jmax(skipButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX, + nextButton.getBestWidthForHeight(buttonHeight) + buttonPaddingX); skipButton.setBounds(footer.removeFromLeft(buttonWidth)); nextButton.setBounds(footer.removeFromRight(buttonWidth)); footer.removeFromRight(10); prevButton.setBounds(footer.removeFromRight(buttonWidth)); - const int middleRightX = prevButton.isVisible() ? (prevButton.getX() - 10) : nextButton.getX(); + const int middleRightX = + prevButton.isVisible() ? (prevButton.getX() - 10) : nextButton.getX(); const int middleX = skipButton.getRight() + 12; const int middleW = jmax(0, middleRightX - middleX); copyrightLabel.setBounds(middleX, footer.getY(), middleW, footer.getHeight()); @@ -603,56 +604,40 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener learnMoreLink.setVisible(false); } } - } -private: - void ensureContentChildrenAttached() - { - if (auto* content = resolveContentRoot()) - { - auto attach = [content](Component& component) - { - if (component.getParentComponent() != content) - content->addAndMakeVisible(component); - }; + Label titleLabel; + TextEditor descriptionEditor; - attach(titleLabel); - attach(descriptionEditor); - attach(learnMoreLink); - attach(copyrightLabel); - attach(nextButton); - attach(prevButton); - attach(skipButton); - attach(dontShowAgainToggle); - attach(pageIndicator); - attach(showDetailsButton); - } - } + TextButton nextButton { "Next" }; + TextButton prevButton { "Back" }; + TextButton skipButton { "Skip Tutorial" }; + ToggleButton dontShowAgainToggle { "Don't show again" }; - void addToContent(Component& component) - { - if (auto* content = resolveContentRoot()) - content->addAndMakeVisible(component); - } + Label pageIndicator; + HyperlinkButton learnMoreLink { "Learn more", + URL("https://harp-plugin.netlify.app/content/intro.html") }; + Label copyrightLabel; - Component* resolveContentRoot() - { - if (contentRoot == nullptr) - contentRoot = getContentComponent(); - return contentRoot; - } + TextButton showDetailsButton { "Show detailed control descriptions" }; + + int currentStep = 0; + bool showingDetails = false; + + private: + WelcomeWindow& owner; + }; void updateStep() { if (steps.empty()) return; - const auto& step = steps[size_t(currentStep)]; + const auto& step = steps[size_t(content->currentStep)]; - titleLabel.setText(step.title, dontSendNotification); - descriptionEditor.setText(step.description); + content->titleLabel.setText(step.title, dontSendNotification); + content->descriptionEditor.setText(step.description); // Scroll to top when changing steps - descriptionEditor.setCaretPosition(0); + content->descriptionEditor.setCaretPosition(0); // Highlight logic { @@ -670,40 +655,41 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } // Buttons - prevButton.setVisible(currentStep > 0); + content->prevButton.setVisible(content->currentStep > 0); - bool isLast = currentStep == (int) steps.size() - 1; - bool isFirst = currentStep == 0; - // bool isSecond = currentStep == 1; // "Select a Model" + bool isLast = content->currentStep == (int) steps.size() - 1; + bool isFirst = content->currentStep == 0; + // bool isSecond = content->currentStep == 1; // "Select a Model" - nextButton.setButtonText(isLast ? "Finish" : "Next"); - skipButton.setVisible(true); - dontShowAgainToggle.setVisible(true); + content->nextButton.setButtonText(isLast ? "Finish" : "Next"); + content->skipButton.setVisible(true); + content->dontShowAgainToggle.setVisible(true); // Items specific to first page - learnMoreLink.setVisible(isFirst); - copyrightLabel.setVisible(isFirst); + content->learnMoreLink.setVisible(isFirst); + content->copyrightLabel.setVisible(isFirst); // Item specific to second page (Step 2) // hostingEditor removed // pyHarpLink removed - pageIndicator.setText("Step " + String(currentStep + 1) + " of " + String(steps.size()), - dontSendNotification); + content->pageIndicator.setText("Step " + String(content->currentStep + 1) + " of " + + String(steps.size()), + dontSendNotification); // Show details button only on Configure Parameters step bool isConfigParams = step.title.contains("Configure Parameters"); - showDetailsButton.setVisible(isConfigParams); + content->showDetailsButton.setVisible(isConfigParams); // Trigger layout update - resized(); + content->resized(); } void nextStep() { - if (currentStep == 1 && mainComponent != nullptr) + if (content->currentStep == 1 && mainComponent != nullptr) { - auto model = mainComponent->getModel(); + auto model = mainComponent->getModelTab()->getModel(); if (! model || ! model->isLoaded()) { pendingTutorialFallbackLoad = true; @@ -712,9 +698,9 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } } - if (currentStep < (int) steps.size() - 1) + if (content->currentStep < (int) steps.size() - 1) { - currentStep++; + content->currentStep++; updateStep(); } else @@ -725,24 +711,20 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener void prevStep() { - if (currentStep > 0) + if (content->currentStep > 0) { - currentStep--; + content->currentStep--; updateStep(); } } - void skipTutorial() - { - finishTutorial(); - } + void skipTutorial() { finishTutorial(); } void finishTutorial() { - if (dontShowAgainToggle.getToggleState()) - { - Settings::setValue("view.showWelcomePopup", 0, true); - } + String showWelcomePopupUponNextStartup = + ! content->dontShowAgainToggle.getToggleState() ? "1" : "0"; + Settings::setValue("view.showWelcomePopup", showWelcomePopupUponNextStartup, true); if (autoLoadedByTutorialFallback && mainComponent != nullptr) mainComponent->resetTutorialAutoLoadedModel(); @@ -750,29 +732,15 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener closeButtonPressed(); } - MainComponent* mainComponent; - Component* contentRoot = nullptr; - std::vector steps; - int currentStep = 0; bool pendingTutorialFallbackLoad = false; bool autoLoadedByTutorialFallback = false; - Label titleLabel; - TextEditor descriptionEditor; // Changed from Label - - // Step 2 specific labels removed + std::vector steps; - TextButton nextButton; - TextButton prevButton; - TextButton skipButton; - ToggleButton dontShowAgainToggle; - Label pageIndicator; - HyperlinkButton learnMoreLink; - // HyperlinkButton pyHarpLink; // Removed - Label copyrightLabel; + MainComponent* mainComponent; + WelcomeContent* content = nullptr; - TextButton showDetailsButton; - bool showingDetails = false; + ComponentBoundsConstrainer constrainer; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WelcomeWindow) }; From c1b14545d27a5ec8547354ffcc878af8c457e3fb Mon Sep 17 00:00:00 2001 From: Saumya Pailwan Date: Sat, 14 Feb 2026 23:32:19 -0600 Subject: [PATCH 13/13] Fix window placement on fullscreen display --- src/MainComponent.cpp | 1 - src/windows/WelcomeWindow.h | 27 ++++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index d3b4165e..b7402618 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -379,7 +379,6 @@ void MainComponent::openWelcomeWindow(bool ensureDefaultModelLoaded) safeThis->welcomeWindow.reset(); }; - safeThis->welcomeWindow->centreWithSize(500, 420); safeThis->welcomeWindow->setVisible(true); safeThis->welcomeWindow->toFront(true); }); diff --git a/src/windows/WelcomeWindow.h b/src/windows/WelcomeWindow.h index 2639dcdf..76ce15d6 100644 --- a/src/windows/WelcomeWindow.h +++ b/src/windows/WelcomeWindow.h @@ -44,13 +44,14 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener setAlwaysOnTop(true); setResizable(true, true); // Still need to set this to fix size with constrainer - centreWithSize(content->getWidth(), content->getHeight()); setConstrainer(&constrainer); constrainer.setMinimumSize(content->getWidth(), content->getHeight()); constrainer.setMaximumSize(content->getWidth(), content->getHeight()); constrainer.setMinimumOnscreenAmounts(40, 40, 40, 40); + positionOnMainComponentDisplay(); + if (mainComponent) { mainComponent->getModelTab()->addChangeListener(this); @@ -479,6 +480,30 @@ class WelcomeWindow : public DocumentWindow, public ChangeListener } private: + void positionOnMainComponentDisplay() + { + const int width = content != nullptr ? content->getWidth() : getWidth(); + const int height = content != nullptr ? content->getHeight() : getHeight(); + setSize(width, height); + + Rectangle targetBounds; + + if (mainComponent != nullptr) + { + if (auto* topLevel = mainComponent->getTopLevelComponent()) + targetBounds = topLevel->getScreenBounds(); + } + + if (targetBounds.isEmpty()) + { + centreWithSize(width, height); + return; + } + + setTopLeftPosition(targetBounds.getCentreX() - width / 2, + targetBounds.getCentreY() - height / 2); + } + class WelcomeContent : public Component { public: