From 6e93d7140351d3baa61c5c9ea6425cf9136724f4 Mon Sep 17 00:00:00 2001 From: derekllanes Date: Wed, 29 Apr 2026 03:00:31 -0400 Subject: [PATCH 1/6] Add support for file_track inputs and integrate pyharp updates --- CMakeLists.txt | 1 + pyharp | 2 +- src/Model.h | 23 +++++++++++++ src/media/FileDisplayComponent.cpp | 55 ++++++++++++++++++++++++++++++ src/media/FileDisplayComponent.h | 25 ++++++++++++++ src/utils/Controls.h | 6 ++++ src/widgets/TrackAreaWidget.h | 15 ++++++++ 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/media/FileDisplayComponent.cpp create mode 100644 src/media/FileDisplayComponent.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b05d238..0d0d0f71 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ target_sources(${PROJECT_NAME} src/media/MediaDisplayComponent.cpp src/media/AudioDisplayComponent.cpp src/media/MidiDisplayComponent.cpp + src/media/FileDisplayComponent.cpp src/media/OutputLabelComponent.cpp src/media/pianoroll/KeyboardComponent.cpp diff --git a/pyharp b/pyharp index be3e7540..d8ef2c0e 160000 --- a/pyharp +++ b/pyharp @@ -1 +1 @@ -Subproject commit be3e7540442e3c85fc1c5d2daf4675f8ffa88916 +Subproject commit d8ef2c0ef42bbef6cfa8362dc786e672324e6fb0 diff --git a/src/Model.h b/src/Model.h index eb205086..bb0a2d56 100644 --- a/src/Model.h +++ b/src/Model.h @@ -440,6 +440,18 @@ class Model DBG_AND_LOG("Model::extractInputs: MIDI track input \"" + midiTrack->label + "\" extracted."); } + // Generic File + else if (type == "file_track") + { + std::shared_ptr fileTrack = + std::make_shared(controlsDict); + + newInputs.push_back(fileTrack); + tempComponentIDs.push_back(fileTrack->id); + + DBG_AND_LOG("Model::extractInputs: File track input \"" + fileTrack->label + + "\" extracted."); + } else if (type == "text_box") { std::shared_ptr textControl = @@ -563,6 +575,17 @@ class Model DBG_AND_LOG("Model::extractOutputs: MIDI track output \"" + midiTrack->label + "\" extracted."); } + // Generic File + else if (type == "file_track") + { + std::shared_ptr fileTrack = + std::make_shared(controlsDict); + + newOutputs.push_back(fileTrack); + + DBG_AND_LOG("Model::extractOutputs: File track output \"" + fileTrack->label + + "\" extracted."); + } else if (type == "json") { // Labels are handled separately diff --git a/src/media/FileDisplayComponent.cpp b/src/media/FileDisplayComponent.cpp new file mode 100644 index 00000000..cd87714f --- /dev/null +++ b/src/media/FileDisplayComponent.cpp @@ -0,0 +1,55 @@ +#include "FileDisplayComponent.h" + +FileDisplayComponent::FileDisplayComponent() + : FileDisplayComponent("File Track") +{ +} + +FileDisplayComponent::FileDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) + : MediaDisplayComponent(name, req, fromDAW, mode) +{ +} + +FileDisplayComponent::~FileDisplayComponent() +{ +} + +StringArray FileDisplayComponent::getSupportedExtensions() +{ + return { + ".nam", + ".txt", + ".csv", + ".json", + ".pth", + ".pt", + ".onnx" + }; +} + +StringArray FileDisplayComponent::getInstanceExtensions() +{ + return FileDisplayComponent::getSupportedExtensions(); +} + +double FileDisplayComponent::getTotalLengthInSecs() +{ + return 0.0; +} + +void FileDisplayComponent::loadMediaFile(const URL& filePath) +{ + setTrackName(filePath.getFileName()); + postLoadActions(filePath); + repaint(); +} + +void FileDisplayComponent::resetMedia() +{ + repaint(); +} + +void FileDisplayComponent::postLoadActions(const URL& /*filePath*/) +{ + // No extra action needed for generic files. +} \ No newline at end of file diff --git a/src/media/FileDisplayComponent.h b/src/media/FileDisplayComponent.h new file mode 100644 index 00000000..5bd907fc --- /dev/null +++ b/src/media/FileDisplayComponent.h @@ -0,0 +1,25 @@ +#pragma once + +#include "MediaDisplayComponent.h" + +class FileDisplayComponent : public MediaDisplayComponent +{ +public: + FileDisplayComponent(); + FileDisplayComponent(String name, + bool req = true, + bool fromDAW = false, + DisplayMode mode = DisplayMode::Input); + ~FileDisplayComponent() override; + + static StringArray getSupportedExtensions(); + + StringArray getInstanceExtensions() override; + + double getTotalLengthInSecs() override; + + void loadMediaFile(const URL& filePath) override; + void resetMedia() override; + + void postLoadActions(const URL& filePath) override; +}; \ No newline at end of file diff --git a/src/utils/Controls.h b/src/utils/Controls.h index 051cb9c0..2aa5a665 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -83,6 +83,12 @@ struct MidiTrackComponentInfo : public TrackComponentInfo using TrackComponentInfo::TrackComponentInfo; }; +// New struct for generic file type +struct FileTrackComponentInfo : public TrackComponentInfo +{ + using TrackComponentInfo::TrackComponentInfo; +}; + struct TextBoxComponentInfo : public ModelComponentInfo, public TextEditor::Listener { std::string value { "" }; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index f81f7765..4d678a1c 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -11,6 +11,7 @@ #include "../media/AudioDisplayComponent.h" #include "../media/MediaDisplayComponent.h" #include "../media/MidiDisplayComponent.h" +#include "../media/FileDisplayComponent.h" #include "../utils/Controls.h" #include "../utils/Interface.h" @@ -214,6 +215,11 @@ class TrackAreaWidget : public Component, m = std::make_unique( label, midiTrackInfo->required, fromDAW, displayMode); } + else if (auto fileTrackInfo = dynamic_cast(trackInfo)) + { + m = std::make_unique( + label, fileTrackInfo->required, fromDAW, displayMode); + } else { DBG_AND_LOG( @@ -306,6 +312,15 @@ class TrackAreaWidget : public Component, trackInfo = std::move(midiTrackInfo); } + else if (FileDisplayComponent::getSupportedExtensions().contains(ext)) + { + auto fileTrackInfo = std::make_unique(); + + fileTrackInfo->required = false; + fileTrackInfo->label = label.toStdString(); + + trackInfo = std::move(fileTrackInfo); + } else { DBG_AND_LOG("TrackAreaWidget::addTrackFromFilePath: Tried to add file " From cfb59599d0410b88713e615964fc9124e2872c2e Mon Sep 17 00:00:00 2001 From: derekllanes Date: Fri, 15 May 2026 15:32:56 -0500 Subject: [PATCH 2/6] Add generic file picker control for file inputs --- CMakeLists.txt | 1 + pyharp | 2 +- src/Model.h | 39 +++++----- src/gui/FilePickerWithLabel.h | 126 ++++++++++++++++++++++++++++++++ src/utils/Controls.h | 28 ++++++- src/widgets/ControlAreaWidget.h | 39 +++++++++- src/widgets/TrackAreaWidget.h | 15 ---- 7 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 src/gui/FilePickerWithLabel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d0d0f71..b92305b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,7 @@ target_sources(${PROJECT_NAME} src/gui/HoverHandler.h src/gui/ControlComponent.h src/gui/ToggleWithLabel.h + src/gui/FilePickerWithLabel.h src/media/MediaDisplayComponent.cpp src/media/AudioDisplayComponent.cpp diff --git a/pyharp b/pyharp index d8ef2c0e..80b265ea 160000 --- a/pyharp +++ b/pyharp @@ -1 +1 @@ -Subproject commit d8ef2c0ef42bbef6cfa8362dc786e672324e6fb0 +Subproject commit 80b265ea1adf3fded4e2f3963a28062215e21e0d diff --git a/src/Model.h b/src/Model.h index bb0a2d56..4a6b6e71 100644 --- a/src/Model.h +++ b/src/Model.h @@ -314,6 +314,22 @@ class Model { controlValue = var(comboBoxComponentInfo->value); } + else if (auto filePickerComponentInfo = + dynamic_cast(componentInfo.get())) + { + if (filePickerComponentInfo->path.empty()) + { + controlValue = var(); + } + else + { + DynamicObject::Ptr fileObj = new DynamicObject(); + fileObj->setProperty("path", var(filePickerComponentInfo->path)); + controlValue = var(fileObj); + } + + wasFile = true; + } else { // Unsupported control was added @@ -441,15 +457,15 @@ class Model + "\" extracted."); } // Generic File - else if (type == "file_track") + else if (type == "file_picker") { - std::shared_ptr fileTrack = - std::make_shared(controlsDict); + std::shared_ptr filePicker = + std::make_shared(controlsDict); - newInputs.push_back(fileTrack); - tempComponentIDs.push_back(fileTrack->id); + newControls.push_back(filePicker); + tempComponentIDs.push_back(filePicker->id); - DBG_AND_LOG("Model::extractInputs: File track input \"" + fileTrack->label + DBG_AND_LOG("Model::extractInputs: File picker control \"" + filePicker->label + "\" extracted."); } else if (type == "text_box") @@ -575,17 +591,6 @@ class Model DBG_AND_LOG("Model::extractOutputs: MIDI track output \"" + midiTrack->label + "\" extracted."); } - // Generic File - else if (type == "file_track") - { - std::shared_ptr fileTrack = - std::make_shared(controlsDict); - - newOutputs.push_back(fileTrack); - - DBG_AND_LOG("Model::extractOutputs: File track output \"" + fileTrack->label - + "\" extracted."); - } else if (type == "json") { // Labels are handled separately diff --git a/src/gui/FilePickerWithLabel.h b/src/gui/FilePickerWithLabel.h new file mode 100644 index 00000000..7cc498aa --- /dev/null +++ b/src/gui/FilePickerWithLabel.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#include "ControlComponent.h" +#include "../utils/Controls.h" + +using namespace juce; + +class FilePickerWithLabel : public ControlComponent, private Button::Listener +{ +public: + explicit FilePickerWithLabel(FilePickerComponentInfo* infoToUse) + : info(infoToUse), + browseButton("Browse...") + { + if (info != nullptr) + { + label.setText(info->label, dontSendNotification); + } + + label.setJustificationType(Justification::centred); + + pathBox.setReadOnly(true); + pathBox.setText("No file selected", dontSendNotification); + pathBox.setMultiLine(false); + pathBox.setScrollbarsShown(false); + pathBox.setCaretVisible(false); + + browseButton.addListener(this); + + addAndMakeVisible(label); + addAndMakeVisible(pathBox); + addAndMakeVisible(browseButton); + } + + ~FilePickerWithLabel() override + { + browseButton.removeListener(this); + } + + void resized() override + { + auto area = getLocalBounds(); + + auto labelArea = area.removeFromTop(20); + label.setBounds(labelArea); + + area.removeFromTop(4); + + auto buttonArea = area.removeFromLeft(90); + browseButton.setBounds(buttonArea); + + area.removeFromLeft(6); + pathBox.setBounds(area); + } + + int getMinimumRequiredWidth() const override + { + const int labelWidth = getLabelWidth(label); + return jmax(260, labelWidth + defaultPadding); + } + +private: + void buttonClicked(Button* button) override + { + if (button != &browseButton || info == nullptr) + { + return; + } + + String pattern = buildWildcardPattern(); + + fileChooser = std::make_unique( + "Select file", + File(), + pattern + ); + + fileChooser->launchAsync( + FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, + [this](const FileChooser& chooser) + { + File selectedFile = chooser.getResult(); + + if (selectedFile.existsAsFile()) + { + info->path = selectedFile.getFullPathName().toStdString(); + pathBox.setText(selectedFile.getFullPathName(), dontSendNotification); + } + } + ); + } + + String buildWildcardPattern() const + { + if (info == nullptr || info->fileTypes.empty()) + { + return "*"; + } + + StringArray patterns; + + for (const auto& ext : info->fileTypes) + { + String extension(ext); + + if (! extension.startsWithChar('.')) + { + extension = "." + extension; + } + + patterns.add("*" + extension); + } + + return patterns.joinIntoString(";"); + } + + FilePickerComponentInfo* info = nullptr; + + Label label; + TextEditor pathBox; + TextButton browseButton; + + std::unique_ptr fileChooser; +}; \ No newline at end of file diff --git a/src/utils/Controls.h b/src/utils/Controls.h index 2aa5a665..c12d1b9d 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -83,10 +83,32 @@ struct MidiTrackComponentInfo : public TrackComponentInfo using TrackComponentInfo::TrackComponentInfo; }; -// New struct for generic file type -struct FileTrackComponentInfo : public TrackComponentInfo +struct FilePickerComponentInfo : public ModelComponentInfo { - using TrackComponentInfo::TrackComponentInfo; + bool required = true; + std::string path { "" }; + std::vector fileTypes; + + FilePickerComponentInfo(DynamicObject* input) : ModelComponentInfo(input) + { + if (input->hasProperty("required")) + { + required = stringToBool(input->getProperty("required").toString()); + } + + if (input->hasProperty("file_types")) + { + Array* types = input->getProperty("file_types").getArray(); + + if (types != nullptr) + { + for (int i = 0; i < types->size(); ++i) + { + fileTypes.push_back(types->getReference(i).toString().toStdString()); + } + } + } + } }; struct TextBoxComponentInfo : public ModelComponentInfo, public TextEditor::Listener diff --git a/src/widgets/ControlAreaWidget.h b/src/widgets/ControlAreaWidget.h index 58c8ec98..d02cc133 100644 --- a/src/widgets/ControlAreaWidget.h +++ b/src/widgets/ControlAreaWidget.h @@ -16,6 +16,7 @@ #include "../gui/SliderWithLabel.h" #include "../gui/TextBoxWithLabel.h" #include "../gui/ToggleWithLabel.h" +#include "../gui/FilePickerWithLabel.h" #include "../utils/Controls.h" #include "../utils/Logging.h" @@ -81,7 +82,7 @@ class ControlAreaWidget : public Component int getNumControls() const { return sliderComponents.size() + toggleComponents.size() + dropdownComponents.size() - + textComponents.size(); + + textComponents.size() + filePickerComponents.size(); } int getMinimumRequiredWidth() const @@ -100,6 +101,7 @@ class ControlAreaWidget : public Component checkGroup(toggleComponents); checkGroup(dropdownComponents); checkGroup(textComponents); + checkGroup(filePickerComponents); return requiredWidth + 2 * (marginSize + minEdgeGap); } @@ -156,6 +158,12 @@ class ControlAreaWidget : public Component } dropdownComponents.clear(); + for (auto& c : filePickerComponents) + { + removeChildComponent(c.get()); + } + filePickerComponents.clear(); + handlers.clear(); } @@ -182,6 +190,10 @@ class ControlAreaWidget : public Component { addDropdown(dropdownInfo); } + else if (auto* filePickerInfo = dynamic_cast(info.get())) + { + addFilePicker(filePickerInfo); + } else { // Unsupported control detected @@ -287,6 +299,18 @@ class ControlAreaWidget : public Component dropdownComponents.push_back(std::move(dropdownComponent)); } + void addFilePicker(FilePickerComponentInfo* info) + { + std::unique_ptr filePickerComponent = + std::make_unique(info); + + addHandler(filePickerComponent.get(), info); + + addAndMakeVisible(*filePickerComponent); + + filePickerComponents.push_back(std::move(filePickerComponent)); + } + void addHandler(Component* comp, ModelComponentInfo* info) { std::unique_ptr handler = std::make_unique(*comp); @@ -341,14 +365,16 @@ class ControlAreaWidget : public Component addGroupToRows(rows, toggleComponents, 1, width); addGroupToRows(rows, dropdownComponents, 2, width); addGroupToRows(rows, textComponents, 3, width); + addGroupToRows(rows, filePickerComponents, 4, width); return rows; } + template void addGroupToRows(std::vector>& rows, - const auto& components, - int type, - int availableWidth) const + const ComponentList& components, + int type, + int availableWidth) const { auto spec = getLayoutSpec(type); @@ -403,6 +429,8 @@ class ControlAreaWidget : public Component return { preferredDropdownWidth, minDropdownHeight }; case 3: return { preferredTextBoxWidth, minTextBoxHeight }; + case 4: + return { preferredFilePickerWidth, minFilePickerHeight }; } return { preferredDropdownWidth, minDropdownHeight }; @@ -426,11 +454,13 @@ class ControlAreaWidget : public Component static constexpr int minToggleHeight = 34; static constexpr int minDropdownHeight = 44; static constexpr int minTextBoxHeight = 84; + static constexpr int minFilePickerHeight = 64; static constexpr int preferredSliderWidth = 108; static constexpr int preferredToggleWidth = 112; static constexpr int preferredDropdownWidth = 140; static constexpr int preferredTextBoxWidth = 200; + static constexpr int preferredFilePickerWidth = 260; static constexpr int minInterItemGap = 6; static constexpr int minEdgeGap = 4; @@ -441,6 +471,7 @@ class ControlAreaWidget : public Component std::vector> toggleComponents; std::vector> sliderComponents; std::vector> dropdownComponents; + std::vector> filePickerComponents; std::vector> handlers; diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index 4d678a1c..f81f7765 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -11,7 +11,6 @@ #include "../media/AudioDisplayComponent.h" #include "../media/MediaDisplayComponent.h" #include "../media/MidiDisplayComponent.h" -#include "../media/FileDisplayComponent.h" #include "../utils/Controls.h" #include "../utils/Interface.h" @@ -215,11 +214,6 @@ class TrackAreaWidget : public Component, m = std::make_unique( label, midiTrackInfo->required, fromDAW, displayMode); } - else if (auto fileTrackInfo = dynamic_cast(trackInfo)) - { - m = std::make_unique( - label, fileTrackInfo->required, fromDAW, displayMode); - } else { DBG_AND_LOG( @@ -312,15 +306,6 @@ class TrackAreaWidget : public Component, trackInfo = std::move(midiTrackInfo); } - else if (FileDisplayComponent::getSupportedExtensions().contains(ext)) - { - auto fileTrackInfo = std::make_unique(); - - fileTrackInfo->required = false; - fileTrackInfo->label = label.toStdString(); - - trackInfo = std::move(fileTrackInfo); - } else { DBG_AND_LOG("TrackAreaWidget::addTrackFromFilePath: Tried to add file " From 784ed042dfb86fc805a8358c28f89fe5d32dab83 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Thu, 28 May 2026 16:02:55 -0400 Subject: [PATCH 3/6] Improved naming scheme and consistency for file chooser component. --- CMakeLists.txt | 2 +- src/Model.h | 26 +++--- ...ckerWithLabel.h => FileChooserWithLabel.h} | 85 +++++++++++-------- src/utils/Controls.h | 26 ++++-- src/widgets/ControlAreaWidget.h | 48 +++++++---- 5 files changed, 113 insertions(+), 74 deletions(-) rename src/gui/{FilePickerWithLabel.h => FileChooserWithLabel.h} (52%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b92305b3..bedbd289 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ target_sources(${PROJECT_NAME} src/gui/HoverHandler.h src/gui/ControlComponent.h src/gui/ToggleWithLabel.h - src/gui/FilePickerWithLabel.h + src/gui/FileChooserWithLabel.h src/media/MediaDisplayComponent.cpp src/media/AudioDisplayComponent.cpp diff --git a/src/Model.h b/src/Model.h index 4a6b6e71..f54093f8 100644 --- a/src/Model.h +++ b/src/Model.h @@ -314,17 +314,18 @@ class Model { controlValue = var(comboBoxComponentInfo->value); } - else if (auto filePickerComponentInfo = - dynamic_cast(componentInfo.get())) + else if (auto fileComponentInfo = dynamic_cast(componentInfo.get())) { - if (filePickerComponentInfo->path.empty()) + if (fileComponentInfo->path.empty()) { controlValue = var(); } else { DynamicObject::Ptr fileObj = new DynamicObject(); - fileObj->setProperty("path", var(filePickerComponentInfo->path)); + + fileObj->setProperty("path", var(fileComponentInfo->path)); + controlValue = var(fileObj); } @@ -456,16 +457,15 @@ class Model DBG_AND_LOG("Model::extractInputs: MIDI track input \"" + midiTrack->label + "\" extracted."); } - // Generic File - else if (type == "file_picker") + else if (type == "generic_file") { - std::shared_ptr filePicker = - std::make_shared(controlsDict); + std::shared_ptr fileChooser = + std::make_shared(controlsDict); - newControls.push_back(filePicker); - tempComponentIDs.push_back(filePicker->id); + newControls.push_back(fileChooser); + tempComponentIDs.push_back(fileChooser->id); - DBG_AND_LOG("Model::extractInputs: File picker control \"" + filePicker->label + DBG_AND_LOG("Model::extractInputs: File chooser control \"" + fileChooser->label + "\" extracted."); } else if (type == "text_box") @@ -591,6 +591,10 @@ class Model DBG_AND_LOG("Model::extractOutputs: MIDI track output \"" + midiTrack->label + "\" extracted."); } + else if (type == "generic_file") + { + // TODO + } else if (type == "json") { // Labels are handled separately diff --git a/src/gui/FilePickerWithLabel.h b/src/gui/FileChooserWithLabel.h similarity index 52% rename from src/gui/FilePickerWithLabel.h rename to src/gui/FileChooserWithLabel.h index 7cc498aa..124517bf 100644 --- a/src/gui/FilePickerWithLabel.h +++ b/src/gui/FileChooserWithLabel.h @@ -1,24 +1,24 @@ +/** + * @file FileChooserWithLabel.h + * @brief Custom file chooser component with label. + * @author derekllanes, cwitkowitz + */ + #pragma once -#include +#include +#include #include "ControlComponent.h" -#include "../utils/Controls.h" using namespace juce; -class FilePickerWithLabel : public ControlComponent, private Button::Listener +class FileChooserWithLabel : public ControlComponent, private Button::Listener { public: - explicit FilePickerWithLabel(FilePickerComponentInfo* infoToUse) - : info(infoToUse), - browseButton("Browse...") + FileChooserWithLabel(const String& labelText = {}) { - if (info != nullptr) - { - label.setText(info->label, dontSendNotification); - } - + label.setText(labelText, dontSendNotification); label.setJustificationType(Justification::centred); pathBox.setReadOnly(true); @@ -34,7 +34,7 @@ class FilePickerWithLabel : public ControlComponent, private Button::Listener addAndMakeVisible(browseButton); } - ~FilePickerWithLabel() override + ~FileChooserWithLabel() override { browseButton.removeListener(this); } @@ -43,50 +43,61 @@ class FilePickerWithLabel : public ControlComponent, private Button::Listener { auto area = getLocalBounds(); - auto labelArea = area.removeFromTop(20); - label.setBounds(labelArea); + label.setBounds(area.removeFromTop(labelHeight)); - area.removeFromTop(4); + area.removeFromTop(labelGap); - auto buttonArea = area.removeFromLeft(90); - browseButton.setBounds(buttonArea); + browseButton.setBounds(area.removeFromLeft(browseButtonWidth)); - area.removeFromLeft(6); + area.removeFromLeft(browseButtonGap); pathBox.setBounds(area); } + void setPath(const String& path) + { + pathBox.setText(path, dontSendNotification); + } + + void setFileTypes(const std::vector& types) + { + fileTypes = types; + } + int getMinimumRequiredWidth() const override { const int labelWidth = getLabelWidth(label); - return jmax(260, labelWidth + defaultPadding); + return jmax(minFilePickerWidth, labelWidth + defaultPadding); } + TextEditor& getPathBox() { return pathBox; } + + /** Called with full path after successful file choice. */ + std::function onFileSelected; + private: void buttonClicked(Button* button) override { - if (button != &browseButton || info == nullptr) - { + if (button != &browseButton) return; - } - - String pattern = buildWildcardPattern(); fileChooser = std::make_unique( "Select file", File(), - pattern + buildWildcardPattern() ); fileChooser->launchAsync( FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, [this](const FileChooser& chooser) { - File selectedFile = chooser.getResult(); + const File selectedFile = chooser.getResult(); if (selectedFile.existsAsFile()) { - info->path = selectedFile.getFullPathName().toStdString(); pathBox.setText(selectedFile.getFullPathName(), dontSendNotification); + + if (onFileSelected) + onFileSelected(selectedFile.getFullPathName()); } } ); @@ -94,21 +105,17 @@ class FilePickerWithLabel : public ControlComponent, private Button::Listener String buildWildcardPattern() const { - if (info == nullptr || info->fileTypes.empty()) - { + if (fileTypes.empty()) return "*"; - } StringArray patterns; - for (const auto& ext : info->fileTypes) + for (const auto& ext : fileTypes) { String extension(ext); if (! extension.startsWithChar('.')) - { extension = "." + extension; - } patterns.add("*" + extension); } @@ -116,11 +123,17 @@ class FilePickerWithLabel : public ControlComponent, private Button::Listener return patterns.joinIntoString(";"); } - FilePickerComponentInfo* info = nullptr; + static constexpr int minFilePickerWidth = 260; + static constexpr int labelHeight = 20; + static constexpr int labelGap = 4; + static constexpr int browseButtonWidth = 90; + static constexpr int browseButtonGap = 6; + + std::vector fileTypes; Label label; TextEditor pathBox; - TextButton browseButton; + TextButton browseButton { "Browse..." }; std::unique_ptr fileChooser; -}; \ No newline at end of file +}; diff --git a/src/utils/Controls.h b/src/utils/Controls.h index c12d1b9d..20a9f8d8 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -83,30 +83,40 @@ struct MidiTrackComponentInfo : public TrackComponentInfo using TrackComponentInfo::TrackComponentInfo; }; -struct FilePickerComponentInfo : public ModelComponentInfo +struct FileComponentInfo : public ModelComponentInfo // TODO - Listener? { - bool required = true; std::string path { "" }; std::vector fileTypes; - FilePickerComponentInfo(DynamicObject* input) : ModelComponentInfo(input) + FileComponentInfo(DynamicObject* input) : ModelComponentInfo(input) { - if (input->hasProperty("required")) + if (input->hasProperty("path")) { - required = stringToBool(input->getProperty("required").toString()); + path = input->getProperty("path").toString().toStdString(); } if (input->hasProperty("file_types")) { Array* types = input->getProperty("file_types").getArray(); - if (types != nullptr) + if (types == nullptr) + { + // TODO - handle error case: couldn't load types + } + + int numTypes = types->size(); + + if (numTypes > 0) { - for (int i = 0; i < types->size(); ++i) + for (int j = 0; j < numTypes; j++) { - fileTypes.push_back(types->getReference(i).toString().toStdString()); + fileTypes.push_back(types->getReference(j).toString().toStdString()); } } + else + { + // TODO - handle error case: no types + } } } }; diff --git a/src/widgets/ControlAreaWidget.h b/src/widgets/ControlAreaWidget.h index d02cc133..d6defcb9 100644 --- a/src/widgets/ControlAreaWidget.h +++ b/src/widgets/ControlAreaWidget.h @@ -12,11 +12,11 @@ #include "../widgets/StatusAreaWidget.h" #include "../gui/ComboBoxWithLabel.h" +#include "../gui/FileChooserWithLabel.h" #include "../gui/HoverHandler.h" #include "../gui/SliderWithLabel.h" #include "../gui/TextBoxWithLabel.h" #include "../gui/ToggleWithLabel.h" -#include "../gui/FilePickerWithLabel.h" #include "../utils/Controls.h" #include "../utils/Logging.h" @@ -82,7 +82,7 @@ class ControlAreaWidget : public Component int getNumControls() const { return sliderComponents.size() + toggleComponents.size() + dropdownComponents.size() - + textComponents.size() + filePickerComponents.size(); + + textComponents.size() + fileChooserComponents.size(); } int getMinimumRequiredWidth() const @@ -101,7 +101,7 @@ class ControlAreaWidget : public Component checkGroup(toggleComponents); checkGroup(dropdownComponents); checkGroup(textComponents); - checkGroup(filePickerComponents); + checkGroup(fileChooserComponents); return requiredWidth + 2 * (marginSize + minEdgeGap); } @@ -158,11 +158,11 @@ class ControlAreaWidget : public Component } dropdownComponents.clear(); - for (auto& c : filePickerComponents) + for (auto& c : fileChooserComponents) { removeChildComponent(c.get()); } - filePickerComponents.clear(); + fileChooserComponents.clear(); handlers.clear(); } @@ -190,9 +190,9 @@ class ControlAreaWidget : public Component { addDropdown(dropdownInfo); } - else if (auto* filePickerInfo = dynamic_cast(info.get())) + else if (auto* fileChooserInfo = dynamic_cast(info.get())) { - addFilePicker(filePickerInfo); + addFileChooser(fileChooserInfo); } else { @@ -299,16 +299,28 @@ class ControlAreaWidget : public Component dropdownComponents.push_back(std::move(dropdownComponent)); } - void addFilePicker(FilePickerComponentInfo* info) + void addFileChooser(FileComponentInfo* info) { - std::unique_ptr filePickerComponent = - std::make_unique(info); + std::unique_ptr fileChooserComponent = + std::make_unique(info->label); - addHandler(filePickerComponent.get(), info); + auto& pathBox = fileChooserComponent->getPathBox(); - addAndMakeVisible(*filePickerComponent); + if (! info->path.empty()) + fileChooserComponent->setPath(info->path); - filePickerComponents.push_back(std::move(filePickerComponent)); + fileChooserComponent->setFileTypes(info->fileTypes); + + fileChooserComponent->onFileSelected = [info](const String& path) + { + info->path = path.toStdString(); + }; + + addHandler(&pathBox, info); + + addAndMakeVisible(*fileChooserComponent); + + fileChooserComponents.push_back(std::move(fileChooserComponent)); } void addHandler(Component* comp, ModelComponentInfo* info) @@ -365,16 +377,16 @@ class ControlAreaWidget : public Component addGroupToRows(rows, toggleComponents, 1, width); addGroupToRows(rows, dropdownComponents, 2, width); addGroupToRows(rows, textComponents, 3, width); - addGroupToRows(rows, filePickerComponents, 4, width); + addGroupToRows(rows, fileChooserComponents, 4, width); return rows; } template void addGroupToRows(std::vector>& rows, - const ComponentList& components, - int type, - int availableWidth) const + const ComponentList& components, + int type, + int availableWidth) const { auto spec = getLayoutSpec(type); @@ -471,7 +483,7 @@ class ControlAreaWidget : public Component std::vector> toggleComponents; std::vector> sliderComponents; std::vector> dropdownComponents; - std::vector> filePickerComponents; + std::vector> fileChooserComponents; std::vector> handlers; From 38541f3dc8d3f0b1ce1aee4dcb02ae12d672fb74 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 29 May 2026 13:51:36 -0400 Subject: [PATCH 4/6] Cleaned up display of file chooser component. --- src/gui/FileChooserWithLabel.h | 195 +++++++++++++++++++++----------- src/gui/TextBoxWithLabel.h | 1 + src/widgets/ControlAreaWidget.h | 6 +- 3 files changed, 133 insertions(+), 69 deletions(-) diff --git a/src/gui/FileChooserWithLabel.h b/src/gui/FileChooserWithLabel.h index 124517bf..2e3d5f12 100644 --- a/src/gui/FileChooserWithLabel.h +++ b/src/gui/FileChooserWithLabel.h @@ -10,58 +10,80 @@ #include #include "ControlComponent.h" +#include "MultiButton.h" using namespace juce; -class FileChooserWithLabel : public ControlComponent, private Button::Listener +class FileChooserWithLabel : public ControlComponent, public FileDragAndDropTarget { public: + ~FileChooserWithLabel() { actionButton.setLookAndFeel(nullptr); } + FileChooserWithLabel(const String& labelText = {}) { label.setText(labelText, dontSendNotification); label.setJustificationType(Justification::centred); - - pathBox.setReadOnly(true); - pathBox.setText("No file selected", dontSendNotification); - pathBox.setMultiLine(false); - pathBox.setScrollbarsShown(false); - pathBox.setCaretVisible(false); - - browseButton.addListener(this); - addAndMakeVisible(label); - addAndMakeVisible(pathBox); - addAndMakeVisible(browseButton); - } - ~FileChooserWithLabel() override - { - browseButton.removeListener(this); + initializeButton(); + addAndMakeVisible(actionButton); } void resized() override { auto area = getLocalBounds(); - label.setBounds(area.removeFromTop(labelHeight)); + int buttonSize = area.getHeight(); + actionButton.setBounds(area.removeFromRight(buttonSize)); + } - area.removeFromTop(labelGap); + void paint(Graphics& g) override + { + auto body = getLocalBounds().withTrimmedTop(labelHeight).toFloat(); + auto bodyInt = body.toNearestInt(); + float r = 3.0f; + + auto bg = findColour(ComboBox::backgroundColourId); + auto outline = findColour(ComboBox::outlineColourId); + auto textCol = findColour(ComboBox::textColourId); + + g.setColour(bg); + g.fillRoundedRectangle(body.reduced(0.5f), r); + g.setColour(outline); + g.drawRoundedRectangle(body.reduced(0.5f), r, 1.0f); + + auto textArea = bodyInt.withTrimmedRight(bodyInt.getHeight()).withTrimmedLeft(4); + + g.setFont(Font(13.0f)); + g.setColour(currentPath.isEmpty() ? textCol.withAlpha(0.4f) : textCol.withAlpha(0.85f)); + g.drawText(currentPath.isEmpty() ? getPlaceholderText() : File(currentPath).getFileName(), + textArea, + Justification::centredLeft, + true); + } - browseButton.setBounds(area.removeFromLeft(browseButtonWidth)); + bool isInterestedInFileDrag(const StringArray&) override { return currentPath.isEmpty(); } - area.removeFromLeft(browseButtonGap); - pathBox.setBounds(area); + void filesDropped(const StringArray& files, int, int) override + { + if (! files.isEmpty()) + { + File f(files[0]); + + if (f.existsAsFile()) + setAndNotify(f.getFullPathName()); + } } void setPath(const String& path) { - pathBox.setText(path, dontSendNotification); + currentPath = path; + actionButton.setMode(currentPath.isEmpty() ? chooseFileModeInfo.displayLabel + : removeFileModeInfo.displayLabel); + repaint(); } - void setFileTypes(const std::vector& types) - { - fileTypes = types; - } + void setFileTypes(const std::vector& types) { fileTypes = types; } int getMinimumRequiredWidth() const override { @@ -69,38 +91,77 @@ class FileChooserWithLabel : public ControlComponent, private Button::Listener return jmax(minFilePickerWidth, labelWidth + defaultPadding); } - TextEditor& getPathBox() { return pathBox; } - - /** Called with full path after successful file choice. */ std::function onFileSelected; private: - void buttonClicked(Button* button) override + void initializeButton() { - if (button != &browseButton) - return; - - fileChooser = std::make_unique( - "Select file", - File(), - buildWildcardPattern() - ); - - fileChooser->launchAsync( - FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, - [this](const FileChooser& chooser) - { - const File selectedFile = chooser.getResult(); - - if (selectedFile.existsAsFile()) - { - pathBox.setText(selectedFile.getFullPathName(), dontSendNotification); - - if (onFileSelected) - onFileSelected(selectedFile.getFullPathName()); - } - } - ); + chooseFileModeInfo = MultiButton::Mode { "ChooseFile", + "Click to choose a file.", + [this] { launchFileChooser(); }, + MultiButton::DrawingMode::IconOnly, + Colours::lightblue, + fontawesome::Folder }; + + removeFileModeInfo = MultiButton::Mode { "RemoveFile", + "Click to remove the selected file.", + [this] { clearFile(); }, + MultiButton::DrawingMode::IconOnly, + Colours::orangered, + fontawesome::Remove }; + + actionButton.addMode(chooseFileModeInfo); + actionButton.addMode(removeFileModeInfo); + actionButton.setMode(chooseFileModeInfo.displayLabel); + + actionButton.setLookAndFeel(&noBorderLAF); + } + + String getPlaceholderText() const + { + if (fileTypes.empty()) + return "No file selected"; + + StringArray exts; + + for (const auto& ext : fileTypes) + { + String e(ext); + exts.add(e.startsWithChar('.') ? e : "." + e); + } + + return "No file selected (" + exts.joinIntoString(", ") + ")"; + } + + void clearFile() + { + setPath({}); + + if (onFileSelected) + onFileSelected({}); + } + + void setAndNotify(const String& path) + { + setPath(path); + + if (onFileSelected) + onFileSelected(path); + } + + void launchFileChooser() + { + fileChooser = std::make_unique("Select file", File(), buildWildcardPattern()); + + fileChooser->launchAsync(FileBrowserComponent::openMode + | FileBrowserComponent::canSelectFiles, + [this](const FileChooser& chooser) + { + const File f = chooser.getResult(); + + if (f.existsAsFile()) + setAndNotify(f.getFullPathName()); + }); } String buildWildcardPattern() const @@ -112,28 +173,32 @@ class FileChooserWithLabel : public ControlComponent, private Button::Listener for (const auto& ext : fileTypes) { - String extension(ext); + String e(ext); - if (! extension.startsWithChar('.')) - extension = "." + extension; + if (! e.startsWithChar('.')) + e = "." + e; - patterns.add("*" + extension); + patterns.add("*" + e); } return patterns.joinIntoString(";"); } + struct NoBorderLookAndFeel : public LookAndFeel_V4 + { + void drawButtonBackground(Graphics&, Button&, const Colour&, bool, bool) override {} + }; + static constexpr int minFilePickerWidth = 260; static constexpr int labelHeight = 20; - static constexpr int labelGap = 4; - static constexpr int browseButtonWidth = 90; - static constexpr int browseButtonGap = 6; - - std::vector fileTypes; + NoBorderLookAndFeel noBorderLAF; Label label; - TextEditor pathBox; - TextButton browseButton { "Browse..." }; + MultiButton actionButton; + MultiButton::Mode chooseFileModeInfo; + MultiButton::Mode removeFileModeInfo; + String currentPath; + std::vector fileTypes; std::unique_ptr fileChooser; }; diff --git a/src/gui/TextBoxWithLabel.h b/src/gui/TextBoxWithLabel.h index 507781a1..ecb22163 100644 --- a/src/gui/TextBoxWithLabel.h +++ b/src/gui/TextBoxWithLabel.h @@ -16,6 +16,7 @@ class TextBoxWithLabel : public ControlComponent TextBoxWithLabel(const String& labelText) { label.setText(labelText, dontSendNotification); + label.setJustificationType(Justification::centred); textBox.setMultiLine(true, true); textBox.setReadOnly(false); diff --git a/src/widgets/ControlAreaWidget.h b/src/widgets/ControlAreaWidget.h index d6defcb9..d3fdef7d 100644 --- a/src/widgets/ControlAreaWidget.h +++ b/src/widgets/ControlAreaWidget.h @@ -304,8 +304,6 @@ class ControlAreaWidget : public Component std::unique_ptr fileChooserComponent = std::make_unique(info->label); - auto& pathBox = fileChooserComponent->getPathBox(); - if (! info->path.empty()) fileChooserComponent->setPath(info->path); @@ -316,7 +314,7 @@ class ControlAreaWidget : public Component info->path = path.toStdString(); }; - addHandler(&pathBox, info); + addHandler(fileChooserComponent.get(), info); addAndMakeVisible(*fileChooserComponent); @@ -466,7 +464,7 @@ class ControlAreaWidget : public Component static constexpr int minToggleHeight = 34; static constexpr int minDropdownHeight = 44; static constexpr int minTextBoxHeight = 84; - static constexpr int minFilePickerHeight = 64; + static constexpr int minFilePickerHeight = 50; static constexpr int preferredSliderWidth = 108; static constexpr int preferredToggleWidth = 112; From 7e4eecbc57831b1f472cd8f141683c272793c9a1 Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Fri, 29 May 2026 14:11:36 -0400 Subject: [PATCH 5/6] Implemented file upload for generic files and made them required by default for processing. --- src/Model.h | 32 ++++++++++++++++++++++++++++++-- src/ModelTab.h | 17 +++++++++++++++++ src/utils/Controls.h | 7 +++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Model.h b/src/Model.h index f54093f8..27984a32 100644 --- a/src/Model.h +++ b/src/Model.h @@ -259,6 +259,32 @@ class Model } } + std::map fileControlRemotePaths; + + for (const auto& controlComponent : controlComponents) + { + if (auto* fileComponentInfo = + dynamic_cast(controlComponent.get())) + { + if (fileComponentInfo->path.empty()) + continue; + + String remoteFilePath; + + result = client->uploadFile( + loadedPath, File(fileComponentInfo->path), remoteFilePath); + + if (result.failed()) + { + setStatus(ModelStatus::FAILURE); + + return result; + } + + fileControlRemotePaths[fileComponentInfo->id] = remoteFilePath.toStdString(); + } + } + /* Extract control values for JSON payload in order */ Array controlValues; @@ -316,7 +342,9 @@ class Model } else if (auto fileComponentInfo = dynamic_cast(componentInfo.get())) { - if (fileComponentInfo->path.empty()) + auto it = fileControlRemotePaths.find(fileComponentInfo->id); + + if (it == fileControlRemotePaths.end() || it->second.empty()) { controlValue = var(); } @@ -324,7 +352,7 @@ class Model { DynamicObject::Ptr fileObj = new DynamicObject(); - fileObj->setProperty("path", var(fileComponentInfo->path)); + fileObj->setProperty("path", var(it->second)); controlValue = var(fileObj); } diff --git a/src/ModelTab.h b/src/ModelTab.h index 3c172ede..a4b4d82f 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -487,6 +487,23 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas } } + for (const auto& controlInfo : model->getControls()) + { + if (auto* fileInfo = dynamic_cast(controlInfo.get())) + { + if (fileInfo->required && fileInfo->path.empty()) + { + AlertWindow::showMessageBoxAsync( + AlertWindow::WarningIcon, + "Error", + "Required file input \"" + String(fileInfo->label) + + "\" is empty. Please select a file before processing."); + + return; + } + } + } + modelSelectionWidget.setDisabled(); processCancelButton.setMode(cancelButtonInfo.displayLabel); diff --git a/src/utils/Controls.h b/src/utils/Controls.h index 20a9f8d8..422b85fc 100644 --- a/src/utils/Controls.h +++ b/src/utils/Controls.h @@ -85,11 +85,18 @@ struct MidiTrackComponentInfo : public TrackComponentInfo struct FileComponentInfo : public ModelComponentInfo // TODO - Listener? { + bool required = true; + std::string path { "" }; std::vector fileTypes; FileComponentInfo(DynamicObject* input) : ModelComponentInfo(input) { + if (input->hasProperty("required")) + { + required = stringToBool(input->getProperty("required").toString()); + } + if (input->hasProperty("path")) { path = input->getProperty("path").toString().toStdString(); From 8304d717727620b885c1139b049aac926474feab Mon Sep 17 00:00:00 2001 From: Frank Cwitkowitz Date: Mon, 1 Jun 2026 12:42:42 -0400 Subject: [PATCH 6/6] Fleshed out support for generic file outputs. --- src/Model.h | 15 +++-- src/ModelTab.h | 4 +- src/media/FileDisplayComponent.cpp | 101 ++++++++++++++++++++++------- src/media/FileDisplayComponent.h | 26 +++++++- src/media/MediaDisplayComponent.h | 3 + src/widgets/ControlAreaWidget.h | 4 +- src/widgets/TrackAreaWidget.h | 32 ++++++++- 7 files changed, 151 insertions(+), 34 deletions(-) diff --git a/src/Model.h b/src/Model.h index 27984a32..ac0e01d7 100644 --- a/src/Model.h +++ b/src/Model.h @@ -263,16 +263,15 @@ class Model for (const auto& controlComponent : controlComponents) { - if (auto* fileComponentInfo = - dynamic_cast(controlComponent.get())) + if (auto* fileComponentInfo = dynamic_cast(controlComponent.get())) { if (fileComponentInfo->path.empty()) continue; String remoteFilePath; - result = client->uploadFile( - loadedPath, File(fileComponentInfo->path), remoteFilePath); + result = + client->uploadFile(loadedPath, File(fileComponentInfo->path), remoteFilePath); if (result.failed()) { @@ -621,7 +620,13 @@ class Model } else if (type == "generic_file") { - // TODO + std::shared_ptr fileOutput = + std::make_shared(controlsDict); + + newOutputs.push_back(fileOutput); + + DBG_AND_LOG("Model::extractOutputs: File output \"" + fileOutput->label + + "\" extracted."); } else if (type == "json") { diff --git a/src/ModelTab.h b/src/ModelTab.h index a4b4d82f..f73a0bba 100644 --- a/src/ModelTab.h +++ b/src/ModelTab.h @@ -553,7 +553,9 @@ class ModelTab : public Component, private ChangeListener, public ChangeBroadcas { auto& outputMediaDisplays = outputTrackAreaWidget.getMediaDisplays(); - for (size_t i = 0; i < outputMediaDisplays.size(); ++i) + for (size_t i = 0; + i < outputMediaDisplays.size() && i < outputFilesPtr->size(); + ++i) { outputMediaDisplays[i]->initializeDisplay( URL((*outputFilesPtr)[i])); diff --git a/src/media/FileDisplayComponent.cpp b/src/media/FileDisplayComponent.cpp index cd87714f..c0d9f2a8 100644 --- a/src/media/FileDisplayComponent.cpp +++ b/src/media/FileDisplayComponent.cpp @@ -1,55 +1,110 @@ #include "FileDisplayComponent.h" -FileDisplayComponent::FileDisplayComponent() - : FileDisplayComponent("File Track") -{ -} +FileDisplayComponent::FileDisplayComponent() : FileDisplayComponent("File Track") {} FileDisplayComponent::FileDisplayComponent(String name, bool req, bool fromDAW, DisplayMode mode) : MediaDisplayComponent(name, req, fromDAW, mode) { -} + downloadActiveMode = MultiButton::Mode { "Download", + "Click to save the output file.", + [this] { saveFileCallback(); }, + MultiButton::DrawingMode::IconOnly, + Colours::lightblue, + fontawesome::Save }; + downloadInactiveMode = + MultiButton::Mode { "Download-Inactive", "No output file available.", + [this] {}, MultiButton::DrawingMode::IconOnly, + Colours::lightgrey, fontawesome::Save }; + downloadButton.addMode(downloadActiveMode); + downloadButton.addMode(downloadInactiveMode); + downloadButton.setMode(downloadInactiveMode.displayLabel); + downloadButton.setLookAndFeel(&noBorderLAF); -FileDisplayComponent::~FileDisplayComponent() -{ + addAndMakeVisible(downloadButton); } +FileDisplayComponent::~FileDisplayComponent() { downloadButton.setLookAndFeel(nullptr); } + StringArray FileDisplayComponent::getSupportedExtensions() { - return { - ".nam", - ".txt", - ".csv", - ".json", - ".pth", - ".pt", - ".onnx" - }; + return {}; // Empty = accept all extensions } StringArray FileDisplayComponent::getInstanceExtensions() { - return FileDisplayComponent::getSupportedExtensions(); + return instanceFileTypes; // Empty = accept all; populated from model metadata when provided } -double FileDisplayComponent::getTotalLengthInSecs() +void FileDisplayComponent::setInstanceFileTypes(const std::vector& types) { - return 0.0; + instanceFileTypes.clear(); + + for (const auto& t : types) + { + String ext(t); + + if (! ext.startsWithChar('.')) + ext = "." + ext; + + instanceFileTypes.add(ext); + } } -void FileDisplayComponent::loadMediaFile(const URL& filePath) +double FileDisplayComponent::getTotalLengthInSecs() { return 0.0; } + +void FileDisplayComponent::paint(Graphics& g) { - setTrackName(filePath.getFileName()); - postLoadActions(filePath); + auto body = getLocalBounds().withTrimmedTop(labelHeight).toFloat(); + auto bodyInt = body.toNearestInt(); + float r = 3.0f; + + auto bg = findColour(ComboBox::backgroundColourId); + auto outline = findColour(ComboBox::outlineColourId); + auto textCol = findColour(ComboBox::textColourId); + + // Draw label area + g.setFont(Font(14.0f)); + g.setColour(textCol); + g.drawText( + getTrackName(), getLocalBounds().removeFromTop(labelHeight), Justification::centred, true); + + // Draw body background + g.setColour(bg); + g.fillRoundedRectangle(body.reduced(0.5f), r); + g.setColour(outline); + g.drawRoundedRectangle(body.reduced(0.5f), r, 1.0f); + + // Draw filename text (leaves space for the square button on the right) + auto textArea = bodyInt.withTrimmedRight(bodyInt.getHeight()).withTrimmedLeft(4); + + g.setFont(Font(13.0f)); + g.setColour(isFileLoaded() ? textCol.withAlpha(0.85f) : textCol.withAlpha(0.4f)); + g.drawText(isFileLoaded() ? getOriginalFilePath().getFileName() : "No output file", + textArea, + Justification::centredLeft, + true); +} + +void FileDisplayComponent::resized() +{ + auto area = getLocalBounds().withTrimmedTop(labelHeight); + int buttonSize = area.getHeight(); + downloadButton.setBounds(area.removeFromRight(buttonSize)); +} + +void FileDisplayComponent::loadMediaFile(const URL& /*filePath*/) +{ + downloadButton.setMode(downloadActiveMode.displayLabel); repaint(); } void FileDisplayComponent::resetMedia() { + downloadButton.setMode(downloadInactiveMode.displayLabel); repaint(); } void FileDisplayComponent::postLoadActions(const URL& /*filePath*/) { // No extra action needed for generic files. -} \ No newline at end of file +} diff --git a/src/media/FileDisplayComponent.h b/src/media/FileDisplayComponent.h index 5bd907fc..579788a7 100644 --- a/src/media/FileDisplayComponent.h +++ b/src/media/FileDisplayComponent.h @@ -16,10 +16,34 @@ class FileDisplayComponent : public MediaDisplayComponent StringArray getInstanceExtensions() override; + int getFixedHeight() const override { return fixedHeight; } + double getTotalLengthInSecs() override; + void paint(Graphics& g) override; + void resized() override; + void loadMediaFile(const URL& filePath) override; void resetMedia() override; void postLoadActions(const URL& filePath) override; -}; \ No newline at end of file + + void setInstanceFileTypes(const std::vector& types); + +private: + struct NoBorderLookAndFeel : public LookAndFeel_V4 + { + void drawButtonBackground(Graphics&, Button&, const Colour&, bool, bool) override {} + }; + + NoBorderLookAndFeel noBorderLAF; + + MultiButton downloadButton; + MultiButton::Mode downloadActiveMode; + MultiButton::Mode downloadInactiveMode; + + StringArray instanceFileTypes; + + static constexpr int fixedHeight = 50; + static constexpr int labelHeight = 20; +}; diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..d58687a9 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -89,6 +89,9 @@ class MediaDisplayComponent : public Component, String getTrackName() { return trackName; } bool isRequired() const { return required; } + + // Returns a fixed height in pixels this display should occupy, or 0 to use flex sizing. + virtual int getFixedHeight() const { return 0; } bool isLinkedToDAW() const { return linkedToDAW; } bool isInputTrack() { return (displayMode == DisplayMode::Input) || isHybridTrack(); } diff --git a/src/widgets/ControlAreaWidget.h b/src/widgets/ControlAreaWidget.h index d3fdef7d..770be1c4 100644 --- a/src/widgets/ControlAreaWidget.h +++ b/src/widgets/ControlAreaWidget.h @@ -310,9 +310,7 @@ class ControlAreaWidget : public Component fileChooserComponent->setFileTypes(info->fileTypes); fileChooserComponent->onFileSelected = [info](const String& path) - { - info->path = path.toStdString(); - }; + { info->path = path.toStdString(); }; addHandler(fileChooserComponent.get(), info); diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index f81f7765..f78ac9f7 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -9,6 +9,7 @@ #include #include "../media/AudioDisplayComponent.h" +#include "../media/FileDisplayComponent.h" #include "../media/MediaDisplayComponent.h" #include "../media/MidiDisplayComponent.h" @@ -51,10 +52,16 @@ class TrackAreaWidget : public Component, { FlexItem i = FlexItem(*m); + int fixedH = m->getFixedHeight(); + if (fixedTrackHeight) { i = i.withHeight(fixedTrackHeight); } + else if (fixedH > 0) + { + i = i.withHeight(fixedH).withFlex(0); + } else { i = i.withFlex(1).withMinHeight(50); @@ -243,6 +250,25 @@ class TrackAreaWidget : public Component, } } + void addFileOutputFromComponentInfo(FileComponentInfo* fileInfo) + { + std::string label = fileInfo->label.empty() ? "File Output" : fileInfo->label; + + auto m = std::make_unique(String(label), false, false, displayMode); + + m->setTrackID(fileInfo->id); + m->setInstanceFileTypes(fileInfo->fileTypes); + + if (! fileInfo->info.empty()) + m->setMediaInstructions(fileInfo->info); + + m->addChangeListener(this); + addAndMakeVisible(m.get()); + mediaDisplays.push_back(std::move(m)); + + resized(); + } + void updateTracks(const ModelComponentInfoList& trackComponents) { resetState(); @@ -253,9 +279,13 @@ class TrackAreaWidget : public Component, { addTrackFromComponentInfo(trackInfo); } + else if (auto* fileInfo = dynamic_cast(info.get())) + { + addFileOutputFromComponentInfo(fileInfo); + } else { - // Invalid input track + // Invalid track component jassertfalse; } }