diff --git a/JUCE b/JUCE index 501c0767..9f643254 160000 --- a/JUCE +++ b/JUCE @@ -1 +1 @@ -Subproject commit 501c07674e1ad693085a7e7c398f205c2677f5da +Subproject commit 9f64325446ec0baf11f0f5f99c8484a15bbd1ab0 diff --git a/src/Application.cpp b/src/Application.cpp index 058e4fbc..46c23426 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -17,6 +17,7 @@ enum CommandIDs // View viewStatusArea = 0x2000, viewMediaClipboard = 0x2001, + viewPreviewPane = 0x2002, // Help about = 0x3000, @@ -71,6 +72,7 @@ PopupMenu MainComponent::getMenuForIndex([[maybe_unused]] int menuIndex, const S { menu.addCommandItem(&commandManager, CommandIDs::viewStatusArea); menu.addCommandItem(&commandManager, CommandIDs::viewMediaClipboard); + menu.addCommandItem(&commandManager, CommandIDs::viewPreviewPane); } else if (menuName == "Help") { @@ -135,7 +137,7 @@ void MainComponent::getAllCommands(Array& commands) commands.addArray(editIDs, numElementsInArray(editIDs)); - const CommandID viewIDs[] = { CommandIDs::viewStatusArea, CommandIDs::viewMediaClipboard }; + const CommandID viewIDs[] = { CommandIDs::viewStatusArea, CommandIDs::viewMediaClipboard, CommandIDs::viewPreviewPane }; commands.addArray(viewIDs, numElementsInArray(viewIDs)); @@ -207,6 +209,12 @@ void MainComponent::getCommandInfo(CommandID commandID, ApplicationCommandInfo& break; + case CommandIDs::viewPreviewPane: + result.setInfo("Preview Pane", "Toggles display of track preview pane", "View", 0); + result.setTicked(mediaClipboardWidget.isPreviewPaneVisible()); + + break; + /* --Help-- */ case CommandIDs::about: result.setInfo( @@ -277,6 +285,11 @@ bool MainComponent::perform(const InvocationInfo& info) break; + case CommandIDs::viewPreviewPane: + DBG_AND_LOG("MainComponent:: perform: \"ViewPreviewPane\" command invoked."); + mediaClipboardWidget.togglePreviewPane(); + break; + /* --Help-- */ case CommandIDs::about: DBG_AND_LOG("MainComponent::perform: \"about\" command invoked."); diff --git a/src/MainComponent.cpp b/src/MainComponent.cpp index 46bf43b9..96e8c0ff 100644 --- a/src/MainComponent.cpp +++ b/src/MainComponent.cpp @@ -536,16 +536,6 @@ Rectangle MainComponent::getClipboardRemoveButtonBounds() return {}; } -Rectangle MainComponent::getClipboardPlayButtonBounds() -{ - if (showMediaClipboard && mediaClipboardWidget.isVisible()) - { - auto bounds = mediaClipboardWidget.getPlayButtonBounds(); - return getLocalArea(&mediaClipboardWidget, bounds); - } - return {}; -} - Rectangle MainComponent::getClipboardSendToDAWButtonBounds() { if (showMediaClipboard && mediaClipboardWidget.isVisible()) diff --git a/src/media/MediaDisplayComponent.cpp b/src/media/MediaDisplayComponent.cpp index 7d9c9023..8ea41d41 100644 --- a/src/media/MediaDisplayComponent.cpp +++ b/src/media/MediaDisplayComponent.cpp @@ -170,7 +170,23 @@ MediaDisplayComponent::MediaDisplayComponent(String name, bool req, bool fromDAW mediaAreaContainer.addAndMakeVisible(contentComponent); mediaAreaContainer.addAndMakeVisible(*timeAxisStrip); mediaAreaContainer.addAndMakeVisible(horizontalScrollBar); - addAndMakeVisible(mediaAreaContainer); + if (isPreviewTrack()) + { + mediaRowComponent.addAndMakeVisible(mediaAreaContainer); + mediaRowComponent.addAndMakeVisible(previewControlsComponent); + addAndMakeVisible(mediaRowComponent); + + mediaRowFlexBox.flexDirection = FlexBox::Direction::row; + mediaRowFlexBox.alignItems = FlexBox::AlignItems::stretch; + + previewControlsFlexBox.flexDirection = FlexBox::Direction::column; + previewControlsFlexBox.alignItems = FlexBox::AlignItems::center; + previewControlsFlexBox.justifyContent = FlexBox::JustifyContent::center; + } + else + { + addAndMakeVisible(mediaAreaContainer); + } mediaAreaFlexBox.flexDirection = FlexBox::Direction::column; @@ -253,6 +269,42 @@ void MediaDisplayComponent::initializeButtons() copyFileButton.addMode(copyFileButtonInactiveInfo); headerComponent.addAndMakeVisible(copyFileButton); + // Modes for the preview pane + if (isPreviewTrack()) + { + previewPlayButtonInfo = MultiButton::Mode { + "Preview-Play", + "Click to start playback.", + [this] { start(); }, + MultiButton::DrawingMode::IconOnly, + Colours::limegreen, + fontaudio::Play + }; + previewPauseButtonInfo = MultiButton::Mode { + "Preview-Pause", + "Click to pause playback.", + [this] { pause(); }, + MultiButton::DrawingMode::IconOnly, + Colours::yellow, + fontaudio::Pause + }; + previewStopButtonInfo = MultiButton::Mode { + "Preview-Stop", + "Click to stop playback.", + [this] { stop(); }, + MultiButton::DrawingMode::IconOnly, + Colours::orangered, + fontaudio::Stop + }; + previewPlayPauseButton.addMode(previewPlayButtonInfo); + previewPlayPauseButton.addMode(previewPauseButtonInfo); + previewStopButton.addMode(previewStopButtonInfo); + previewStopButton.setMode(previewStopButtonInfo.displayLabel); + + previewControlsComponent.addAndMakeVisible(previewPlayPauseButton); + previewControlsComponent.addAndMakeVisible(previewStopButton); + } + resetButtonState(); } @@ -331,6 +383,14 @@ void MediaDisplayComponent::resized() .withHeight(trackNameLabel.getFont().getHeight()) .withMargin(1)); } + else if (isPreviewTrack()) + { + // Place header over media + mainFlexBox.flexDirection = FlexBox::Direction::column; + mainFlexBox.items.add(FlexItem(headerComponent) + .withHeight(trackNameLabel.getFont().getHeight()) + .withMargin(1)); + } else { // Place header beside media @@ -339,18 +399,34 @@ void MediaDisplayComponent::resized() mainFlexBox.items.add(FlexItem(headerComponent).withFlex(1).withMaxWidth(40).withMargin(4)); } - // Media area takes remaining space - mainFlexBox.items.add(FlexItem(mediaAreaContainer).withFlex(8)); + // Media area takes remaining space unless preview pane is open + if (isPreviewTrack()) + { + mainFlexBox.items.add(FlexItem(mediaRowComponent).withFlex(8)); + } + else + { + mainFlexBox.items.add(FlexItem(mediaAreaContainer).withFlex(8)); + } mainFlexBox.performLayout(totalBounds); + if (isPreviewTrack()) + { + mediaRowFlexBox.items.clear(); + mediaRowFlexBox.items.add(FlexItem(mediaAreaContainer).withFlex(1)); + mediaRowFlexBox.items.add( + FlexItem(previewControlsComponent).withWidth(25).withMargin({ 0, 2, 0, 2 })); + mediaRowFlexBox.performLayout(mediaRowComponent.getLocalBounds()); + } + // Remove existing items in header flex headerFlexBox.items.clear(); // Add track label to header flex headerFlexBox.items.add(FlexItem(trackNameLabel).withFlex(1).withMargin({ 0, 2, 0, 0 })); - if (! isThumbnailTrack()) + if (! isThumbnailTrack() && ! isPreviewTrack()) { // Add buttons to header flex headerFlexBox.items.add(FlexItem(buttonsComponent).withFlex(2).withMargin({ 0, 0, 0, 1 })); @@ -363,7 +439,7 @@ void MediaDisplayComponent::resized() float trackNameLabelWidth = labelBounds.getWidth(); float trackNameLabelHeight = labelBounds.getHeight(); - if (! isThumbnailTrack()) + if (! isThumbnailTrack() && ! isPreviewTrack()) { Point labelCenter = labelBounds.getCentre(); // Rotate track name label 90 degrees @@ -454,6 +530,18 @@ void MediaDisplayComponent::resized() // Perform layout in media area mediaAreaFlexBox.performLayout(mediaAreaContainer.getLocalBounds()); + // Performs layout in preview pane + if (isPreviewTrack()) + { + previewControlsFlexBox.items.clear(); + previewControlsFlexBox.items.add( + FlexItem(previewPlayPauseButton).withWidth(25).withHeight(25).withMargin({ 2, 0, 2, 0 })); + previewControlsFlexBox.items.add( + FlexItem(previewStopButton).withWidth(25).withHeight(25).withMargin({ 2, 0, 2, 0 })); + previewControlsFlexBox.performLayout(previewControlsComponent.getLocalBounds()); + } + + if (! isLabelRepositioningScheduled) { isLabelRepositioningScheduled = true; @@ -605,6 +693,10 @@ void MediaDisplayComponent::resetButtonState() chooseFileButton.setMode(chooseFileButtonActiveInfo.displayLabel); saveFileButton.setMode(saveFileButtonInactiveInfo.displayLabel); copyFileButton.setMode(copyFileButtonInactiveInfo.displayLabel); + if (isPreviewTrack()) + { + previewPlayPauseButton.setMode(previewPlayButtonInfo.displayLabel); + } } void MediaDisplayComponent::initializeDisplay(const URL& filePath) @@ -1191,6 +1283,11 @@ void MediaDisplayComponent::start() startTimerHz(40); playStopButton.setMode(stopButtonInfo.displayLabel); + + if (isPreviewTrack()) + { + previewPlayPauseButton.setMode(previewPauseButtonInfo.displayLabel); + } } void MediaDisplayComponent::stop() @@ -1204,9 +1301,24 @@ void MediaDisplayComponent::stop() playStopButton.setMode(playButtonActiveInfo.displayLabel); + if (isPreviewTrack()) + { + previewPlayPauseButton.setMode(previewPlayButtonInfo.displayLabel); + } + sendChangeMessage(); } +void MediaDisplayComponent::pause() +{ + stopPlaying(); + + stopTimer(); + + //currentPositionCursor.setVisible(true); + previewPlayPauseButton.setMode(previewPlayButtonInfo.displayLabel); +} + void MediaDisplayComponent::updateCursorPosition() { float mediaWidth = getMediaWidth(); @@ -1234,14 +1346,11 @@ void MediaDisplayComponent::updateCursorPosition() cursorPositionX = jmin(maxCursorXPos, jmax(minCursorXPos, cursorPositionX)); - Rectangle mediaAreaBounds = mediaAreaContainer.getBounds(); Rectangle mediaBounds = contentComponent.getBounds(); - - // Include offset(s) for track header - cursorPositionX += static_cast(mediaAreaBounds.getX()); - cursorPositionY += static_cast(mediaAreaBounds.getY()); - // Include offset for label overlay header - cursorPositionY += static_cast(mediaBounds.getY()); + // Get position of contentComponent in the correct coordinate space, accounting for nested parents + Point mediaOrigin = getLocalPoint(&contentComponent, Point(0, 0)); + cursorPositionX += static_cast(mediaOrigin.getX()); + cursorPositionY += static_cast(mediaOrigin.getY()); // Offset by half of width cursorPositionX -= cursorWidth / 2.0f; diff --git a/src/media/MediaDisplayComponent.h b/src/media/MediaDisplayComponent.h index 39b91e58..37879718 100644 --- a/src/media/MediaDisplayComponent.h +++ b/src/media/MediaDisplayComponent.h @@ -24,7 +24,8 @@ enum class DisplayMode Input, Output, Hybrid, // All functionality - Thumbnail // Reduced functionality + Thumbnail, // Reduced functionality + Preview // For preview pane }; class ColorablePanel : public Component @@ -95,6 +96,7 @@ class MediaDisplayComponent : public Component, bool isOutputTrack() { return (displayMode == DisplayMode::Output) || isHybridTrack(); } bool isHybridTrack() { return displayMode == DisplayMode::Hybrid; } bool isThumbnailTrack() { return displayMode == DisplayMode::Thumbnail; } + bool isPreviewTrack() { return displayMode == DisplayMode::Preview; } void setMediaInstructions(String instructions) { mediaInstructions = instructions; } @@ -140,6 +142,7 @@ class MediaDisplayComponent : public Component, void start(); void stop(); + void pause(); virtual bool isPlaying() { return transportSource.isPlaying(); } @@ -188,7 +191,15 @@ class MediaDisplayComponent : public Component, void timerCallback() override; virtual void visibleRangeCallback() { repaint(); } - virtual void changeListenerCallback(ChangeBroadcaster*) override { repaint(); } + virtual void changeListenerCallback(ChangeBroadcaster*) override + { + repaint(); + Component::SafePointer safeThis(this); + MessageManager::callAsync([safeThis]() mutable { + if (safeThis != nullptr) + safeThis->sendChangeMessage(); + }); + } virtual void resetMedia() = 0; void resetPaths(); @@ -255,6 +266,10 @@ class MediaDisplayComponent : public Component, Component buttonsComponent; // Media + overhead panel (if any) Component mediaAreaContainer; + // Preview pane controls + Component previewControlsComponent; + // Preview pane waveform and control row + Component mediaRowComponent; // Header sub-components Label trackNameLabel; @@ -272,6 +287,13 @@ class MediaDisplayComponent : public Component, MultiButton::Mode copyFileButtonActiveInfo; MultiButton::Mode copyFileButtonInactiveInfo; + // Preview mode controls + MultiButton previewPlayPauseButton; + MultiButton::Mode previewPlayButtonInfo; + MultiButton::Mode previewPauseButtonInfo; + MultiButton previewStopButton; + MultiButton::Mode previewStopButtonInfo; + // Panel displaying overhead labels ColorablePanel overheadPanel { overheadPanelColor }; @@ -283,6 +305,10 @@ class MediaDisplayComponent : public Component, FlexBox buttonsFlexBox; // Flex for media / overhead panel (if any) FlexBox mediaAreaFlexBox; + // Flex for preview pane buttons + FlexBox previewControlsFlexBox; + // Flex for preview pane waveform + control row + FlexBox mediaRowFlexBox; Uuid trackID; String trackName; diff --git a/src/widgets/MediaClipboardWidget.h b/src/widgets/MediaClipboardWidget.h index 19f3fcf8..d011d4a9 100644 --- a/src/widgets/MediaClipboardWidget.h +++ b/src/widgets/MediaClipboardWidget.h @@ -14,6 +14,8 @@ #include "../utils/Logging.h" +#include "PreviewPaneWidget.h" + using namespace juce; class MediaClipboardWidget : public Component, public ChangeListener @@ -34,6 +36,18 @@ class MediaClipboardWidget : public Component, public ChangeListener trackAreaWidget.addChangeListener(this); trackArea.setViewedComponent(&trackAreaWidget, false); addAndMakeVisible(trackArea); + + addAndMakeVisible(previewPaneWidget); + previewPaneWidget.onClose = [this] + { + showPreviewPane = false; + resized(); + }; + previewPaneWidget.onResize = [this](int newHeight) + { + previewPaneHeight = newHeight; + resized(); + }; } ~MediaClipboardWidget() { trackAreaWidget.removeChangeListener(this); } @@ -55,6 +69,15 @@ class MediaClipboardWidget : public Component, public ChangeListener .withMargin(marginSize)); //jmax(30, trackNameLabel.getFont().getHeight())) mainFlexBox.items.add( FlexItem(trackArea).withFlex(10).withMargin({ 0, marginSize, marginSize, marginSize })); + if (showPreviewPane) + { + mainFlexBox.items.add( + FlexItem(previewPaneWidget).withHeight(previewPaneHeight).withMargin({ 0, marginSize, marginSize, marginSize })); + } + else + { + previewPaneWidget.setBounds(0, 0, 0, 0); + } mainFlexBox.performLayout(totalBounds); @@ -66,7 +89,7 @@ class MediaClipboardWidget : public Component, public ChangeListener // Add control elements to control flex controlsFlexBox.items.add(FlexItem(selectionTextBox).withFlex(1)); controlsFlexBox.items.add( - FlexItem(buttonsComponent).withWidth(5 * (buttonWidth + marginSize))); + FlexItem(buttonsComponent).withWidth(4 * (buttonWidth + marginSize))); controlsFlexBox.performLayout(controlsComponent.getLocalBounds()); @@ -89,10 +112,6 @@ class MediaClipboardWidget : public Component, public ChangeListener .withWidth(buttonWidth) .withHeight(buttonWidth) .withMargin({ 0, 0, 0, marginSize })); - buttonsFlexBox.items.add(FlexItem(playStopButton) - .withWidth(buttonWidth) - .withHeight(buttonWidth) - .withMargin({ 0, 0, 0, marginSize })); buttonsFlexBox.items.add(FlexItem(saveFileButton) .withWidth(buttonWidth) .withHeight(buttonWidth) @@ -160,14 +179,14 @@ class MediaClipboardWidget : public Component, public ChangeListener return getLocalArea(&buttonsComponent, removeSelectionButton.getBounds()).expanded(2); } - Rectangle getPlayButtonBounds() const + Rectangle getSendToDAWButtonBounds() const { - return getLocalArea(&buttonsComponent, playStopButton.getBounds()).expanded(2); + return getLocalArea(&buttonsComponent, sendToDAWButton.getBounds()).expanded(2); } - Rectangle getSendToDAWButtonBounds() const + bool isPreviewPaneVisible() const { - return getLocalArea(&buttonsComponent, sendToDAWButton.getBounds()).expanded(2); + return showPreviewPane; } void addFileCallback() @@ -364,6 +383,17 @@ class MediaClipboardWidget : public Component, public ChangeListener }); } + void togglePreviewPane() + { + showPreviewPane = !showPreviewPane; + if (showPreviewPane) + { + previewPaneWidget.resetMinimizeState(); + previewPaneHeight = previewPaneWidget.getExpandedHeight(); + } + resized(); + } + private: void initializeButtons() { @@ -418,30 +448,6 @@ class MediaClipboardWidget : public Component, public ChangeListener removeSelectionButton.addMode(removeSelectionButtonInactiveInfo); buttonsComponent.addAndMakeVisible(removeSelectionButton); - // Mode when a playable track is selected (play enabled) - playButtonActiveInfo = MultiButton::Mode { "Play-Active", - "Click to start playback.", - [this] { playCallback(); }, - MultiButton::DrawingMode::IconOnly, - Colours::limegreen, - fontaudio::Play }; - // Mode when there is no track selected (play disabled) - playButtonInactiveInfo = - MultiButton::Mode { "Play-Inactive", "Nothing to play.", - [this] {}, MultiButton::DrawingMode::IconOnly, - Colours::lightgrey, fontaudio::Play }; - // Mode during playback (stop enabled) - stopButtonInfo = MultiButton::Mode { "Stop", - "Click to stop playback.", - [this] { stopCallback(); }, - MultiButton::DrawingMode::IconOnly, - Colours::orangered, - fontaudio::Stop }; - playStopButton.addMode(playButtonActiveInfo); - playStopButton.addMode(playButtonInactiveInfo); - playStopButton.addMode(stopButtonInfo); - buttonsComponent.addAndMakeVisible(playStopButton); - // Mode when a track is selected (save file enabled) saveFileButtonActiveInfo = MultiButton::Mode { "Save-Active", @@ -502,34 +508,32 @@ class MediaClipboardWidget : public Component, public ChangeListener { MediaDisplayComponent* mediaDisplay = trackAreaWidget.getCurrentlySelectedDisplay(); - if (currentlySelectedDisplay) + if (currentlySelectedDisplay && currentlySelectedDisplay->isPlaying()) { - if (currentlySelectedDisplay->isPlaying()) - { - // Cancel playback and reset play/stop button state for select and stop events - stopCallback(currentlySelectedDisplay); - } - else - { - // Reset play/stop button state for select and stop events (avoid infinite messages) - playStopButton.setMode(playButtonActiveInfo.displayLabel); - } + currentlySelectedDisplay->stop(); } if (mediaDisplay) { if (mediaDisplay != currentlySelectedDisplay) { - // Handle select events if selected display changes selectTrack(mediaDisplay); - // Handle track area resizing after adding a track - resized(); // TODO - decouple from track selection? + resized(); + previewPaneWidget.showTrack(mediaDisplay); + } + else if (!previewPaneWidget.isShowingTrack() && mediaDisplay->isFileLoaded()) + { + // File finished loading after initial showTrack was called too early + previewPaneWidget.showTrack(mediaDisplay); } } else { // Handle deselect events resetState(); + + // Clear the preview pane when nothing is selected + previewPaneWidget.clearTrack(); } } @@ -559,33 +563,6 @@ class MediaClipboardWidget : public Component, public ChangeListener } } - void playCallback() - { - MediaDisplayComponent* mediaDisplay = trackAreaWidget.getCurrentlySelectedDisplay(); - - if (mediaDisplay) - { - mediaDisplay->start(); - - playStopButton.setMode(stopButtonInfo.displayLabel); - } - } - - void stopCallback(MediaDisplayComponent* mediaDisplay = nullptr) - { - if (! mediaDisplay) - { - mediaDisplay = trackAreaWidget.getCurrentlySelectedDisplay(); - } - - if (mediaDisplay) - { - mediaDisplay->stop(); - - playStopButton.setMode(playButtonActiveInfo.displayLabel); - } - } - // TODO - move to utils/? bool convertAudioFile(const File& source, const File& target) { @@ -669,7 +646,6 @@ class MediaClipboardWidget : public Component, public ChangeListener //renameSelectionButton.setMode(renameSelectionButtonInactiveInfo.label); addFileButton.setMode(addFileButtonInfo.displayLabel); removeSelectionButton.setMode(removeSelectionButtonInactiveInfo.displayLabel); - playStopButton.setMode(playButtonInactiveInfo.displayLabel); saveFileButton.setMode(saveFileButtonInactiveInfo.displayLabel); sendToDAWButton.setMode(sendToDAWButtonInactiveInfo1.displayLabel); @@ -683,7 +659,6 @@ class MediaClipboardWidget : public Component, public ChangeListener //renameSelectionButton.setMode(renameSelectionButtonActiveInfo.label); removeSelectionButton.setMode(removeSelectionButtonActiveInfo.displayLabel); - playStopButton.setMode(playButtonActiveInfo.displayLabel); saveFileButton.setMode(saveFileButtonActiveInfo.displayLabel); int nOtherDAWLinkedTracks = @@ -727,11 +702,6 @@ class MediaClipboardWidget : public Component, public ChangeListener MultiButton::Mode removeSelectionButtonActiveInfo; MultiButton::Mode removeSelectionButtonInactiveInfo; - MultiButton playStopButton; - MultiButton::Mode playButtonActiveInfo; - MultiButton::Mode playButtonInactiveInfo; - MultiButton::Mode stopButtonInfo; - MultiButton saveFileButton; MultiButton::Mode saveFileButtonActiveInfo; MultiButton::Mode saveFileButtonInactiveInfo; @@ -747,6 +717,10 @@ class MediaClipboardWidget : public Component, public ChangeListener Viewport trackArea; TrackAreaWidget trackAreaWidget { DisplayMode::Thumbnail, 75 }; + PreviewPaneWidget previewPaneWidget; + int previewPaneHeight = 150; + bool showPreviewPane = true; + std::unique_ptr chooseFileBrowser; MediaDisplayComponent* currentlySelectedDisplay; diff --git a/src/widgets/PreviewPaneWidget.h b/src/widgets/PreviewPaneWidget.h new file mode 100644 index 00000000..acdc3c80 --- /dev/null +++ b/src/widgets/PreviewPaneWidget.h @@ -0,0 +1,278 @@ +/** + * @file PreviewPaneWidget.h + * @brief Component that allows for the previewing of MediaComponents. + * @author NatalieElizabeth + */ + +#pragma once + +#include "../media/AudioDisplayComponent.h" +#include "../media/MediaDisplayComponent.h" +#include "../media/MidiDisplayComponent.h" +#include + +using namespace juce; + +class PreviewPaneWidget : public Component, public ChangeListener +{ +public: + PreviewPaneWidget() + { + minimizeButton.setButtonText("-"); + minimizeButton.setTooltip("Minimize preview pane"); + minimizeButton.onClick = [this] + { + if (currentDisplay != nullptr && currentDisplay->isPlaying()) + { + currentDisplay->stop(); + } + if (isMinimized) + { + isMinimized = false; + minimizeButton.setButtonText("-"); + if (onResize) + { + onResize(expandedHeight); + } + } + else + { + expandedHeight = getHeight(); + isMinimized = true; + + minimizeButton.setButtonText(CharPointer_UTF8("\xe2\x96\xb2")); + if (onResize) + { + onResize(titleBarHeight); + } + } + }; + addAndMakeVisible(minimizeButton); + + closeButton.setButtonText(CharPointer_UTF8("\xc3\x97")); + closeButton.setTooltip("Close preview pane"); + closeButton.onClick = [this] + { + if (currentDisplay != nullptr && currentDisplay->isPlaying()) + { + currentDisplay->stop(); + } + + isMinimized = false; + minimizeButton.setButtonText("-"); + + if (onClose) + { + onClose(); + } + }; + addAndMakeVisible(closeButton); + } + + void showTrack(MediaDisplayComponent* source) + { + // Remove previosuly displayed track + if (currentDisplay != nullptr) + { + currentDisplay->removeChangeListener(this); + removeChildComponent(currentDisplay.get()); + currentDisplay.reset(); + } + + if (source == nullptr) + { + repaint(); + return; + } + + URL filePath = source->getOriginalFilePath(); + + if (!filePath.isLocalFile()) + { + repaint(); + return; + } + + if (dynamic_cast(source) != nullptr) + { + auto* newDisplay = + new AudioDisplayComponent(source->getTrackName(), false, false, DisplayMode::Preview); + currentDisplay.reset(newDisplay); + + addAndMakeVisible(newDisplay); + currentDisplay->addChangeListener(this); + + if (filePath.isLocalFile()) + { + newDisplay->initializeDisplay(filePath); + } + } + else + { + auto* newDisplay = + new MidiDisplayComponent(source->getTrackName(), false, false, DisplayMode::Preview); + currentDisplay.reset(newDisplay); + + addAndMakeVisible(newDisplay); + currentDisplay->addChangeListener(this); + + if (filePath.isLocalFile()) + { + newDisplay->initializeDisplay(filePath); + } + } + + resized(); + repaint(); + } + + void clearTrack() + { + if (currentDisplay != nullptr) + { + currentDisplay->removeChangeListener(this); + removeChildComponent(currentDisplay.get()); + currentDisplay.reset(); + } + repaint(); + } + + void changeListenerCallback(ChangeBroadcaster* source) override + { + if (source == currentDisplay.get()) + { + resized(); + repaint(); + } + } + + void resetMinimizeState() + { + isMinimized = false; + minimizeButton.setButtonText("-"); + } + + int getExpandedHeight() const { return expandedHeight; } + + bool isShowingTrack() const { return currentDisplay != nullptr && currentDisplay->isFileLoaded(); } + + // Callbacks + + std::function onClose; + + std::function onResize; + + void paint(Graphics& g) override + { + // Widget background + g.fillAll(Colour(Colours::darkgrey)); + + // Title bar background + Rectangle titleArea(0, 0, getWidth(), titleBarHeight); + g.setColour(getUIColourIfAvailable(LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); + g.fillRect(titleArea); + + // Title bar bottom border + g.setColour(Colours::black.withAlpha(0.4f)); + g.drawLine(0, titleBarHeight, getWidth(), titleBarHeight, 1.0f); + + // Draw "Preview" label + g.setColour(Colours::lightgrey); + g.setFont(Font(12.0f).boldened()); + Rectangle labelArea(8, 0, getWidth() - 8 - 2 * buttonWidth - 3 * buttonMargin, titleBarHeight); + g.drawText("Preview", labelArea, Justification::centredLeft); + + // No track loaded placeholder message + if (currentDisplay == nullptr) + { + g.setColour(Colours::grey); + g.drawText("No track selected.", getLocalBounds(), Justification::centred); + } + } + + void resized() override + { + // Position the close button flush against the right edge of the title bar + int closeX = getWidth() - buttonMargin - buttonWidth; + closeButton.setBounds(closeX, (titleBarHeight - buttonWidth) / 2, buttonWidth, buttonWidth); + + // Position the minimize button immediately to the left of the close button + int minimizeX = closeX - buttonMargin - buttonWidth; + minimizeButton.setBounds(minimizeX, (titleBarHeight - buttonWidth) / 2, buttonWidth, buttonWidth); + + if (!isMinimized && currentDisplay != nullptr) + currentDisplay->setBounds(0, titleBarHeight, getWidth(), getHeight() - titleBarHeight); + } + + // Mouse handling for top-edge resize + + // Called once when the user first presses the mouse button down. + // We record the starting Y position and the starting height so we can + // compute deltas during the drag. + void mouseDown(const MouseEvent& e) override + { + if (isInResizeZone(e.y) && !isMinimized) + { + dragStartY = e.getScreenY(); // absolute screen Y when drag begins + dragStartHeight = getHeight(); // height of this component at drag start + } + } + + // Called repeatedly as the mouse moves while a button is held down. + // We compute how far the mouse has moved upward and ask MainComponent + // to change the pane height accordingly. + void mouseDrag(const MouseEvent& e) override + { + if (dragStartY < 0 && !isMinimized) // drag wasn't initiated in the resize zone + return; + + // A positive delta means the mouse moved down (pane would shrink), + // a negative delta means the mouse moved up (pane would grow). + int delta = e.getScreenY() - dragStartY; + int newHeight = jmax(minimumPaneHeight, dragStartHeight - delta); + + if (onResize) + onResize(newHeight); + } + + // Called whenever the mouse moves (without a button held). + // We use this to switch the cursor to a resize cursor when hovering + // near the top edge, and back to the default otherwise. + void mouseMove(const MouseEvent& e) override + { + if (isInResizeZone(e.y) && !isMinimized) + setMouseCursor(MouseCursor::UpDownResizeCursor); + else + setMouseCursor(MouseCursor::NormalCursor); + } + + // Called when the mouse button is released — reset drag tracking state. + void mouseUp(const MouseEvent& e) override + { + dragStartY = -1; + dragStartHeight = -1; + } + +private: + static constexpr int titleBarHeight = 22; // height of the title bar strip + static constexpr int buttonWidth = 16; // width and height of each title button + static constexpr int buttonMargin = 4; // space between/around title buttons + static constexpr int resizeZoneHeight = 6; // px from top edge that trigger resize cursor + static constexpr int minimumPaneHeight = 60; // smallest the pane can be dragged to + + TextButton minimizeButton; + TextButton closeButton; + std::unique_ptr currentDisplay; + + // Negative sentinel values mean no drag is in progress + int dragStartY = -1; + int dragStartHeight = -1; + + bool isMinimized = false; + int expandedHeight = 150; + + bool isInResizeZone(int localY) const + { + return localY < resizeZoneHeight; + } +}; \ No newline at end of file diff --git a/src/widgets/TrackAreaWidget.h b/src/widgets/TrackAreaWidget.h index e15f3c05..5f57f7a6 100644 --- a/src/widgets/TrackAreaWidget.h +++ b/src/widgets/TrackAreaWidget.h @@ -437,7 +437,7 @@ class TrackAreaWidget : public Component, } String ext = f.getFileExtension(); - String label = filePath.getFileName(); + String label = URL::removeEscapeChars(filePath.getFileName()); bool validExt = true; @@ -473,7 +473,7 @@ class TrackAreaWidget : public Component, { addTrackFromComponentInfo(trackInfo.get(), fromDAW); mediaDisplays.back()->initializeDisplay(filePath); - mediaDisplays.back()->setTrackName(filePath.getFileName()); + mediaDisplays.back()->setTrackName(URL::removeEscapeChars(filePath.getFileName())); } }