From a53d81404e48964cb41acba89b53ca6ae9a4f3c3 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Wed, 9 Sep 2026 10:04:28 +0200 Subject: [PATCH 1/6] Move notification classes into a namespace to avoid repetition --- src/AudioCapture.cpp | 6 ++--- src/AudioCaptureImpl_CoreAudioTap.mm | 4 +-- src/AudioCaptureImpl_SDL.cpp | 4 +-- src/AudioCaptureImpl_WASAPI.cpp | 4 +-- src/ProjectMWrapper.cpp | 24 ++++++++--------- src/ProjectMWrapper.h | 12 ++++----- src/RenderLoop.cpp | 20 +++++++------- src/RenderLoop.h | 6 ++--- src/SDLRenderingWindow.cpp | 2 +- src/SDLRenderingWindow.h | 6 ++--- src/gui/MainMenu.cpp | 22 ++++++++-------- src/gui/ProjectMGUI.cpp | 2 +- src/gui/ProjectMGUI.h | 6 ++--- src/gui/SettingsWindow.cpp | 8 +++--- ...otification.cpp => AudioDataAvailable.cpp} | 16 +++++++----- ...bleNotification.h => AudioDataAvailable.h} | 12 ++++++--- src/notifications/CMakeLists.txt | 20 +++++++------- src/notifications/DisplayToast.cpp | 20 ++++++++++++++ ...playToastNotification.h => DisplayToast.h} | 10 ++++--- .../DisplayToastNotification.cpp | 16 ------------ src/notifications/PlaybackControl.cpp | 26 +++++++++++++++++++ ...ontrolNotification.h => PlaybackControl.h} | 8 ++++-- .../PlaybackControlNotification.cpp | 22 ---------------- src/notifications/Quit.cpp | 10 +++++++ .../{QuitNotification.h => Quit.h} | 6 ++++- src/notifications/QuitNotification.cpp | 6 ----- src/notifications/UpdateWindowTitle.cpp | 10 +++++++ ...itleNotification.h => UpdateWindowTitle.h} | 6 ++++- .../UpdateWindowTitleNotification.cpp | 6 ----- 29 files changed, 180 insertions(+), 140 deletions(-) rename src/notifications/{AudioDataAvailableNotification.cpp => AudioDataAvailable.cpp} (54%) rename src/notifications/{AudioDataAvailableNotification.h => AudioDataAvailable.h} (80%) create mode 100644 src/notifications/DisplayToast.cpp rename src/notifications/{DisplayToastNotification.h => DisplayToast.h} (58%) delete mode 100644 src/notifications/DisplayToastNotification.cpp create mode 100644 src/notifications/PlaybackControl.cpp rename src/notifications/{PlaybackControlNotification.h => PlaybackControl.h} (72%) delete mode 100644 src/notifications/PlaybackControlNotification.cpp create mode 100644 src/notifications/Quit.cpp rename src/notifications/{QuitNotification.h => Quit.h} (64%) delete mode 100644 src/notifications/QuitNotification.cpp create mode 100644 src/notifications/UpdateWindowTitle.cpp rename src/notifications/{UpdateWindowTitleNotification.h => UpdateWindowTitle.h} (62%) delete mode 100644 src/notifications/UpdateWindowTitleNotification.cpp diff --git a/src/AudioCapture.cpp b/src/AudioCapture.cpp index 41680527..3ffc60d4 100644 --- a/src/AudioCapture.cpp +++ b/src/AudioCapture.cpp @@ -4,7 +4,7 @@ #include "ProjectMWrapper.h" -#include "notifications/DisplayToastNotification.h" +#include "notifications/DisplayToast.h" #include @@ -47,7 +47,7 @@ void AudioCapture::NextAudioDevice() if (_impl) { _impl->NextAudioDevice(); - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(_impl->AudioDeviceName())); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(_impl->AudioDeviceName())); } } @@ -56,7 +56,7 @@ void AudioCapture::AudioDeviceIndex(int index) if (_impl) { _impl->AudioDeviceIndex(index); - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(_impl->AudioDeviceName())); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(_impl->AudioDeviceName())); } } diff --git a/src/AudioCaptureImpl_CoreAudioTap.mm b/src/AudioCaptureImpl_CoreAudioTap.mm index d4e9da89..814ffb3e 100644 --- a/src/AudioCaptureImpl_CoreAudioTap.mm +++ b/src/AudioCaptureImpl_CoreAudioTap.mm @@ -1,6 +1,6 @@ #include "AudioCaptureImpl_CoreAudioTap.h" -#include "notifications/AudioDataAvailableNotification.h" +#include "notifications/AudioDataAvailable.h" #include @@ -213,7 +213,7 @@ unsigned int sampleCount = buffer.mDataByteSize / sizeof(float); Poco::NotificationCenter::defaultCenter().postNotification( - new AudioDataAvailableNotification(bufferChannels, samples, sampleCount)); + new Notification::AudioDataAvailable(bufferChannels, samples, sampleCount)); }); if (status != noErr || _ioProcID == nullptr) diff --git a/src/AudioCaptureImpl_SDL.cpp b/src/AudioCaptureImpl_SDL.cpp index f815a49a..885153e4 100644 --- a/src/AudioCaptureImpl_SDL.cpp +++ b/src/AudioCaptureImpl_SDL.cpp @@ -1,6 +1,6 @@ #include "AudioCaptureImpl_SDL.h" -#include "notifications/AudioDataAvailableNotification.h" +#include "notifications/AudioDataAvailable.h" #include #include @@ -163,7 +163,7 @@ void AudioCaptureImpl::AudioInputCallback(void* userData, unsigned char* stream, unsigned int samples = len / sizeof(float); Poco::NotificationCenter::defaultCenter().postNotification( - new AudioDataAvailableNotification(instance->_channels, + new Notification::AudioDataAvailable(instance->_channels, reinterpret_cast(stream), samples)); } diff --git a/src/AudioCaptureImpl_WASAPI.cpp b/src/AudioCaptureImpl_WASAPI.cpp index 3cdeac86..40436262 100644 --- a/src/AudioCaptureImpl_WASAPI.cpp +++ b/src/AudioCaptureImpl_WASAPI.cpp @@ -1,6 +1,6 @@ #include "AudioCaptureImpl_WASAPI.h" -#include "notifications/AudioDataAvailableNotification.h" +#include "notifications/AudioDataAvailable.h" #include @@ -448,7 +448,7 @@ void AudioCaptureImpl::CaptureThread() if (framesAvailable > 0 && data != nullptr) { Poco::NotificationCenter::defaultCenter().postNotification( - new AudioDataAvailableNotification(_channels, + new Notification::AudioDataAvailable(_channels, reinterpret_cast(data), framesAvailable * _channels)); } diff --git a/src/ProjectMWrapper.cpp b/src/ProjectMWrapper.cpp index f176a348..1487c21d 100644 --- a/src/ProjectMWrapper.cpp +++ b/src/ProjectMWrapper.cpp @@ -3,7 +3,7 @@ #include "ProjectMSDLApplication.h" #include "SDLRenderingWindow.h" -#include "notifications/DisplayToastNotification.h" +#include "notifications/DisplayToast.h" #include #include @@ -201,7 +201,7 @@ void ProjectMWrapper::ChangeBeatSensitivity(float value) { projectm_set_beat_sensitivity(_projectM, projectm_get_beat_sensitivity(_projectM) + value); Poco::NotificationCenter::defaultCenter().postNotification( - new DisplayToastNotification(Poco::format("Beat Sensitivity: %.2hf", projectm_get_beat_sensitivity(_projectM)))); + new Notification::DisplayToast(Poco::format("Beat Sensitivity: %.2hf", projectm_get_beat_sensitivity(_projectM)))); } std::string ProjectMWrapper::ProjectMBuildVersion() @@ -232,50 +232,50 @@ void ProjectMWrapper::PresetSwitchedEvent(bool isHardCut, unsigned int index, vo poco_information_f1(that->_logger, "Displaying preset: %s", std::string(presetName)); projectm_playlist_free_string(presetName); - Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::UpdateWindowTitle); } -void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) +void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) { bool shuffleEnabled = projectm_playlist_get_shuffle(_playlist); switch (notification->ControlAction()) { - case PlaybackControlNotification::Action::NextPreset: + case Notification::PlaybackControl::Action::NextPreset: projectm_playlist_set_shuffle(_playlist, false); projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); projectm_playlist_set_shuffle(_playlist, shuffleEnabled); break; - case PlaybackControlNotification::Action::PreviousPreset: + case Notification::PlaybackControl::Action::PreviousPreset: projectm_playlist_set_shuffle(_playlist, false); projectm_playlist_play_previous(_playlist, !notification->SmoothTransition()); projectm_playlist_set_shuffle(_playlist, shuffleEnabled); break; - case PlaybackControlNotification::Action::LastPreset: + case Notification::PlaybackControl::Action::LastPreset: projectm_playlist_play_last(_playlist, !notification->SmoothTransition()); break; - case PlaybackControlNotification::Action::RandomPreset: { + case Notification::PlaybackControl::Action::RandomPreset: { projectm_playlist_set_shuffle(_playlist, true); projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); projectm_playlist_set_shuffle(_playlist, shuffleEnabled); break; } - case PlaybackControlNotification::Action::ToggleShuffle: + case Notification::PlaybackControl::Action::ToggleShuffle: _userConfig->setBool("projectM.shuffleEnabled", !shuffleEnabled); break; - case PlaybackControlNotification::Action::TogglePresetLocked: { + case Notification::PlaybackControl::Action::TogglePresetLocked: { _userConfig->setBool("projectM.presetLocked", !projectm_get_preset_locked(_projectM)); break; } } } -void ProjectMWrapper::AudioDataAvailableNotificationHandler(const Poco::AutoPtr& notification) +void ProjectMWrapper::AudioDataAvailableNotificationHandler(const Poco::AutoPtr& notification) { Poco::ScopedLock lock(_audioBufferMutex); @@ -327,7 +327,7 @@ void ProjectMWrapper::OnConfigurationPropertyRemoved(const std::string& key) if (key == "projectM.presetLocked") { projectm_set_preset_locked(_projectM, _projectMConfigView->getBool("presetLocked", false)); - Poco::NotificationCenter::defaultCenter().postNotification(new UpdateWindowTitleNotification); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::UpdateWindowTitle); } if (key == "projectM.shuffleEnabled") diff --git a/src/ProjectMWrapper.h b/src/ProjectMWrapper.h index c8eb89d8..109efdce 100644 --- a/src/ProjectMWrapper.h +++ b/src/ProjectMWrapper.h @@ -1,7 +1,7 @@ #pragma once -#include "notifications/AudioDataAvailableNotification.h" -#include "notifications/PlaybackControlNotification.h" +#include "notifications/AudioDataAvailable.h" +#include "notifications/PlaybackControl.h" #include #include @@ -90,9 +90,9 @@ class ProjectMWrapper : public Poco::Util::Subsystem */ static void PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context); - void PlaybackControlNotificationHandler(const Poco::AutoPtr& notification); + void PlaybackControlNotificationHandler(const Poco::AutoPtr& notification); - void AudioDataAvailableNotificationHandler(const Poco::AutoPtr& notification); + void AudioDataAvailableNotificationHandler(const Poco::AutoPtr& notification); std::vector GetPathListWithDefault(const std::string& baseKey, const std::string& defaultPath); @@ -118,8 +118,8 @@ class ProjectMWrapper : public Poco::Util::Subsystem std::vector _audioStagingBuffer; //!< Buffer which receives audio data from the capture implementation. mutable Poco::Mutex _audioBufferMutex; //!< Mutex protecting access to the audio staging buffer. - Poco::NObserver _playbackControlNotificationObserver{*this, &ProjectMWrapper::PlaybackControlNotificationHandler}; - Poco::NObserver _audioDataAvailableNotificationObserver{*this, &ProjectMWrapper::AudioDataAvailableNotificationHandler}; + Poco::NObserver _playbackControlNotificationObserver{*this, &ProjectMWrapper::PlaybackControlNotificationHandler}; + Poco::NObserver _audioDataAvailableNotificationObserver{*this, &ProjectMWrapper::AudioDataAvailableNotificationHandler}; Poco::Logger& _logger{Poco::Logger::get("SDLRenderingWindow")}; //!< The class logger. }; diff --git a/src/RenderLoop.cpp b/src/RenderLoop.cpp index 7d428e0f..083ed8b1 100644 --- a/src/RenderLoop.cpp +++ b/src/RenderLoop.cpp @@ -136,7 +136,7 @@ void RenderLoop::PollEvents() if (!droppedFile.exists() || (droppedFileP.getExtension() != "milk" && droppedFileP.getExtension() != "prjm")) { std::string toastMessage = std::string("Invalid preset file: ") + droppedFilePath; - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(toastMessage)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); poco_information_f1(_logger, "%s", toastMessage); break; // exit the block and go to the shuffle check } @@ -173,12 +173,12 @@ void RenderLoop::PollEvents() // if skip to dropped is true, or if a folder was dropped and it overrode the playlist, we skip to the next preset projectm_playlist_play_next(_playlistHandle, true); } - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(toastMessage)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); } else { std::string toastMessage = std::string("No presets found in: ") + droppedFilePath; - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(toastMessage)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); poco_information_f1(_logger, "%s", toastMessage); } } @@ -312,15 +312,15 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) break; case SDLK_n: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::NextPreset, _keyStates._shiftPressed)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::NextPreset, _keyStates._shiftPressed)); break; case SDLK_p: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::PreviousPreset, _keyStates._shiftPressed)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::PreviousPreset, _keyStates._shiftPressed)); break; case SDLK_r: { - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset, _keyStates._shiftPressed)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::RandomPreset, _keyStates._shiftPressed)); break; } @@ -332,15 +332,15 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) break; case SDLK_y: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::ToggleShuffle)); break; case SDLK_BACKSPACE: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset, _keyStates._shiftPressed)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::LastPreset, _keyStates._shiftPressed)); break; case SDLK_SPACE: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::TogglePresetLocked)); + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::TogglePresetLocked)); break; case SDLK_UP: @@ -425,7 +425,7 @@ void RenderLoop::MouseUpEvent(const SDL_MouseButtonEvent& event) } } -void RenderLoop::QuitNotificationHandler(const Poco::AutoPtr& notification) +void RenderLoop::QuitNotificationHandler(const Poco::AutoPtr& notification) { _wantsToQuit = true; } diff --git a/src/RenderLoop.h b/src/RenderLoop.h index 9429b1ff..e62f579c 100644 --- a/src/RenderLoop.h +++ b/src/RenderLoop.h @@ -4,7 +4,7 @@ #include "ProjectMWrapper.h" #include "SDLRenderingWindow.h" -#include "notifications/QuitNotification.h" +#include "notifications/Quit.h" #include #include @@ -70,7 +70,7 @@ class RenderLoop * @brief Handler for quit notifications. * @param notification The received notification. */ - void QuitNotificationHandler(const Poco::AutoPtr& notification); + void QuitNotificationHandler(const Poco::AutoPtr& notification); AudioCapture& _audioCapture; ProjectMWrapper& _projectMWrapper; @@ -81,7 +81,7 @@ class RenderLoop ProjectMGUI& _projectMGui; - Poco::NObserver _quitNotificationObserver{*this, &RenderLoop::QuitNotificationHandler}; //!< The observer for quit notifications. + Poco::NObserver _quitNotificationObserver{*this, &RenderLoop::QuitNotificationHandler}; //!< The observer for quit notifications. bool _wantsToQuit{false}; diff --git a/src/SDLRenderingWindow.cpp b/src/SDLRenderingWindow.cpp index 5328ff00..d9273ed6 100644 --- a/src/SDLRenderingWindow.cpp +++ b/src/SDLRenderingWindow.cpp @@ -384,7 +384,7 @@ SDL_GLContext SDLRenderingWindow::GetGlContext() const return _glContext; } -void SDLRenderingWindow::UpdateWindowTitleNotificationHandler(POCO_UNUSED const Poco::AutoPtr& notification) +void SDLRenderingWindow::UpdateWindowTitleNotificationHandler(POCO_UNUSED const Poco::AutoPtr& notification) { UpdateWindowTitle(); } diff --git a/src/SDLRenderingWindow.h b/src/SDLRenderingWindow.h index 06c3bbee..090b8b48 100644 --- a/src/SDLRenderingWindow.h +++ b/src/SDLRenderingWindow.h @@ -1,6 +1,6 @@ #pragma once -#include "notifications/UpdateWindowTitleNotification.h" +#include "notifications/UpdateWindowTitle.h" #include @@ -117,7 +117,7 @@ class SDLRenderingWindow : public Poco::Util::Subsystem * @brief Receives notifications requesting an update of the window title. * @param notification The update notification. */ - void UpdateWindowTitleNotificationHandler(const Poco::AutoPtr& notification); + void UpdateWindowTitleNotificationHandler(const Poco::AutoPtr& notification); /** * @brief Updates the window title. @@ -147,7 +147,7 @@ class SDLRenderingWindow : public Poco::Util::Subsystem SDL_Window* _renderingWindow{ nullptr }; //!< Pointer to the SDL window used for rendering. SDL_GLContext _glContext{ nullptr }; //!< Pointer to the OpenGL context associated with the window. - Poco::NObserver _updateWindowTitleObserver{*this, &SDLRenderingWindow::UpdateWindowTitleNotificationHandler}; //!< the observer for title update notifications + Poco::NObserver _updateWindowTitleObserver{*this, &SDLRenderingWindow::UpdateWindowTitleNotificationHandler}; //!< the observer for title update notifications Poco::Logger& _logger{ Poco::Logger::get("SDLRenderingWindow") }; //!< The class logger. diff --git a/src/gui/MainMenu.cpp b/src/gui/MainMenu.cpp index 02afba65..20ca2502 100644 --- a/src/gui/MainMenu.cpp +++ b/src/gui/MainMenu.cpp @@ -7,9 +7,9 @@ #include "gui/ProjectMGUI.h" #include "gui/SystemBrowser.h" -#include "notifications/PlaybackControlNotification.h" -#include "notifications/QuitNotification.h" -#include "notifications/UpdateWindowTitleNotification.h" +#include "notifications/PlaybackControl.h" +#include "notifications/Quit.h" +#include "notifications/UpdateWindowTitle.h" #include "imgui.h" @@ -39,7 +39,7 @@ void MainMenu::Draw() if (ImGui::MenuItem("Quit projectM", "Ctrl+q")) { - _notificationCenter.postNotification(new QuitNotification); + _notificationCenter.postNotification(new Notification::Quit); } ImGui::EndMenu(); @@ -51,30 +51,30 @@ void MainMenu::Draw() if (ImGui::MenuItem("Play Next Preset", "n")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::LastPreset)); } if (ImGui::MenuItem("Play Previous Preset", "p")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::PreviousPreset)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::PreviousPreset)); } if (ImGui::MenuItem("Go Back One Preset", "Backspace")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::LastPreset)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::LastPreset)); } if (ImGui::MenuItem("Random Preset", "r")) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::RandomPreset)); } ImGui::Separator(); if (ImGui::MenuItem("Lock Preset", "Spacebar", app.config().getBool("projectM.presetLocked", false))) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::TogglePresetLocked)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::TogglePresetLocked)); } if (ImGui::MenuItem("Enable Shuffle", "y", app.config().getBool("projectM.shuffleEnabled", true))) { - _notificationCenter.postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); + _notificationCenter.postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::ToggleShuffle)); } ImGui::Separator(); @@ -115,7 +115,7 @@ void MainMenu::Draw() if (ImGui::MenuItem("Display Preset Name in Window Title", "", app.config().getBool("window.displayPresetNameInTitle", true))) { app.UserConfiguration()->setBool("window.displayPresetNameInTitle", !app.config().getBool("window.displayPresetNameInTitle", true)); - _notificationCenter.postNotification(new UpdateWindowTitleNotification); + _notificationCenter.postNotification(new Notification::UpdateWindowTitle); } ImGui::Separator(); diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 925c8438..92c3e9a5 100644 --- a/src/gui/ProjectMGUI.cpp +++ b/src/gui/ProjectMGUI.cpp @@ -234,7 +234,7 @@ float ProjectMGUI::GetClampedUserScalingFactor() return std::min(3.0f, std::max(0.1f, static_cast(Poco::Util::Application::instance().config().getDouble("window.uiScale", 1.0)))); } -void ProjectMGUI::DisplayToastNotificationHandler(const Poco::AutoPtr& notification) +void ProjectMGUI::DisplayToastNotificationHandler(const Poco::AutoPtr& notification) { if (Poco::Util::Application::instance().config().getBool("projectM.displayToasts", true)) { diff --git a/src/gui/ProjectMGUI.h b/src/gui/ProjectMGUI.h index aca9d885..3d04478b 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -6,7 +6,7 @@ #include "SettingsWindow.h" #include "ToastMessage.h" -#include "notifications/DisplayToastNotification.h" +#include "notifications/DisplayToast.h" #include @@ -109,11 +109,11 @@ class ProjectMGUI : public Poco::Util::Subsystem static float GetClampedUserScalingFactor(); - void DisplayToastNotificationHandler(const Poco::AutoPtr& notification); + void DisplayToastNotificationHandler(const Poco::AutoPtr& notification); ProjectMWrapper* _projectMWrapper{nullptr}; - Poco::NObserver _displayToastNotificationObserver{*this, &ProjectMGUI::DisplayToastNotificationHandler}; + Poco::NObserver _displayToastNotificationObserver{*this, &ProjectMGUI::DisplayToastNotificationHandler}; std::string _uiIniFileName; //!< Path and filename of the UI configuration (positions etc.) diff --git a/src/gui/SettingsWindow.cpp b/src/gui/SettingsWindow.cpp index 36cfc3b1..5ba9db6e 100644 --- a/src/gui/SettingsWindow.cpp +++ b/src/gui/SettingsWindow.cpp @@ -6,7 +6,7 @@ #include "ProjectMGUI.h" -#include "notifications/DisplayToastNotification.h" +#include "notifications/DisplayToast.h" #include @@ -331,18 +331,18 @@ void SettingsWindow::SaveButton() { _userConfiguration->save(configFile); Poco::NotificationCenter::defaultCenter().postNotification( - new DisplayToastNotification("Settings saved!")); + new Notification::DisplayToast("Settings saved!")); } else { Poco::NotificationCenter::defaultCenter().postNotification( - new DisplayToastNotification("Error saving settings")); + new Notification::DisplayToast("Error saving settings")); } } catch (...) { Poco::NotificationCenter::defaultCenter().postNotification( - new DisplayToastNotification("Error saving settings")); + new Notification::DisplayToast("Error saving settings")); } _changed = false; diff --git a/src/notifications/AudioDataAvailableNotification.cpp b/src/notifications/AudioDataAvailable.cpp similarity index 54% rename from src/notifications/AudioDataAvailableNotification.cpp rename to src/notifications/AudioDataAvailable.cpp index 2b9a341c..51794773 100644 --- a/src/notifications/AudioDataAvailableNotification.cpp +++ b/src/notifications/AudioDataAvailable.cpp @@ -1,15 +1,17 @@ -#include "AudioDataAvailableNotification.h" +#include "AudioDataAvailable.h" #include #include -std::string AudioDataAvailableNotification::name() const +namespace Notification { + +std::string AudioDataAvailable::name() const { return "AudioDataAvailableNotification"; } -AudioDataAvailableNotification::AudioDataAvailableNotification(uint32_t channels, const float* samples, const uint32_t sampleCount) +AudioDataAvailable::AudioDataAvailable(uint32_t channels, const float* samples, const uint32_t sampleCount) : _channels(channels) { poco_assert(channels > 0); @@ -21,12 +23,14 @@ AudioDataAvailableNotification::AudioDataAvailableNotification(uint32_t channels std::memcpy(_samples.data(), samples, sampleCount * sizeof(float)); } -uint32_t AudioDataAvailableNotification::Channels() const +uint32_t AudioDataAvailable::Channels() const { return _channels; } -const std::vector& AudioDataAvailableNotification::Samples() const +const std::vector& AudioDataAvailable::Samples() const { return _samples; -} \ No newline at end of file +} + +} // namespace Notification diff --git a/src/notifications/AudioDataAvailableNotification.h b/src/notifications/AudioDataAvailable.h similarity index 80% rename from src/notifications/AudioDataAvailableNotification.h rename to src/notifications/AudioDataAvailable.h index 4f23bc0c..7a627c93 100644 --- a/src/notifications/AudioDataAvailableNotification.h +++ b/src/notifications/AudioDataAvailable.h @@ -5,25 +5,27 @@ #include #include +namespace Notification { + /** * @brief Passes new audio data to all active projectM instances. * @note Channels must be >0 and samples must contain at least one audio frame/sample. */ -class AudioDataAvailableNotification : public Poco::Notification +class AudioDataAvailable : public Poco::Notification { public: std::string name() const override; - AudioDataAvailableNotification() = delete; + AudioDataAvailable() = delete; /** - * @brief Creates a new AudioDataAvailableNotification instance + * @brief Creates a new AudioDataAvailable notification instance * The number of @a samples must be divisible by @a channels. * @param channels The channel count of the current capture device. * @param samples The new audio samples captured from the active audio device. * @param sampleCount The number of float samples passed to @a samples. */ - explicit AudioDataAvailableNotification(uint32_t channels, const float* samples, uint32_t sampleCount); + explicit AudioDataAvailable(uint32_t channels, const float* samples, uint32_t sampleCount); /** * Returns the number of audio channels of the recording device. @@ -41,3 +43,5 @@ class AudioDataAvailableNotification : public Poco::Notification uint32_t _channels; //!< Number of audio channels of the capture device. std::vector _samples; //!< The new audio samples, containing frames x channels floats. }; + +} // namespace Notification diff --git a/src/notifications/CMakeLists.txt b/src/notifications/CMakeLists.txt index 3064db30..e85ec145 100644 --- a/src/notifications/CMakeLists.txt +++ b/src/notifications/CMakeLists.txt @@ -1,14 +1,14 @@ add_library(ProjectMSDL-Notifications STATIC - AudioDataAvailableNotification.cpp - AudioDataAvailableNotification.h - DisplayToastNotification.cpp - DisplayToastNotification.h - PlaybackControlNotification.cpp - PlaybackControlNotification.h - QuitNotification.cpp - QuitNotification.h - UpdateWindowTitleNotification.cpp - UpdateWindowTitleNotification.h + AudioDataAvailable.cpp + AudioDataAvailable.h + DisplayToast.cpp + DisplayToast.h + PlaybackControl.cpp + PlaybackControl.h + Quit.cpp + Quit.h + UpdateWindowTitle.cpp + UpdateWindowTitle.h ) target_include_directories(ProjectMSDL-Notifications diff --git a/src/notifications/DisplayToast.cpp b/src/notifications/DisplayToast.cpp new file mode 100644 index 00000000..973dbdbf --- /dev/null +++ b/src/notifications/DisplayToast.cpp @@ -0,0 +1,20 @@ +#include "DisplayToast.h" + +namespace Notification { + +DisplayToast::DisplayToast(std::string toastText) + : _toastText(std::move(toastText)) +{ +} + +std::string DisplayToast::name() const +{ + return "DisplayToastNotification"; +} + +const std::string& DisplayToast::ToastText() const +{ + return _toastText; +} + +} // namespace Notification diff --git a/src/notifications/DisplayToastNotification.h b/src/notifications/DisplayToast.h similarity index 58% rename from src/notifications/DisplayToastNotification.h rename to src/notifications/DisplayToast.h index 8b492c88..443f23dc 100644 --- a/src/notifications/DisplayToastNotification.h +++ b/src/notifications/DisplayToast.h @@ -2,20 +2,24 @@ #include +namespace Notification { + /** * @brief Informs the GUI subsystem to queue a new toast message. */ -class DisplayToastNotification : public Poco::Notification +class DisplayToast : public Poco::Notification { public: std::string name() const override; - DisplayToastNotification() = delete; + DisplayToast() = delete; - explicit DisplayToastNotification(std::string toastText); + explicit DisplayToast(std::string toastText); const std::string& ToastText() const; private: std::string _toastText; }; + +} // namespace Notification diff --git a/src/notifications/DisplayToastNotification.cpp b/src/notifications/DisplayToastNotification.cpp deleted file mode 100644 index 467d3b11..00000000 --- a/src/notifications/DisplayToastNotification.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "DisplayToastNotification.h" - -DisplayToastNotification::DisplayToastNotification(std::string toastText) - : _toastText(std::move(toastText)) -{ -} - -std::string DisplayToastNotification::name() const -{ - return "DisplayToastNotification"; -} - -const std::string& DisplayToastNotification::ToastText() const -{ - return _toastText; -} diff --git a/src/notifications/PlaybackControl.cpp b/src/notifications/PlaybackControl.cpp new file mode 100644 index 00000000..1693db5f --- /dev/null +++ b/src/notifications/PlaybackControl.cpp @@ -0,0 +1,26 @@ +#include "PlaybackControl.h" + +namespace Notification { + +PlaybackControl::PlaybackControl(PlaybackControl::Action action, bool smoothTransition) + : _action(action) + , _smoothTransition(smoothTransition) +{ +} + +std::string PlaybackControl::name() const +{ + return "PlaybackControlNotification"; +} + +PlaybackControl::Action PlaybackControl::ControlAction() const +{ + return _action; +} + +bool PlaybackControl::SmoothTransition() const +{ + return _smoothTransition; +} + +} // namespace Notification diff --git a/src/notifications/PlaybackControlNotification.h b/src/notifications/PlaybackControl.h similarity index 72% rename from src/notifications/PlaybackControlNotification.h rename to src/notifications/PlaybackControl.h index d76078ca..c37f1b7f 100644 --- a/src/notifications/PlaybackControlNotification.h +++ b/src/notifications/PlaybackControl.h @@ -2,10 +2,12 @@ #include +namespace Notification { + /** * @brief Navigates the playlist and toggles playback modes. */ -class PlaybackControlNotification : public Poco::Notification +class PlaybackControl : public Poco::Notification { public: enum class Action @@ -18,7 +20,7 @@ class PlaybackControlNotification : public Poco::Notification TogglePresetLocked }; - explicit PlaybackControlNotification(Action action, bool smoothTransition = false); + explicit PlaybackControl(Action action, bool smoothTransition = false); std::string name() const override; @@ -30,3 +32,5 @@ class PlaybackControlNotification : public Poco::Notification Action _action; bool _smoothTransition{}; }; + +} // namespace Notification diff --git a/src/notifications/PlaybackControlNotification.cpp b/src/notifications/PlaybackControlNotification.cpp deleted file mode 100644 index 629c01fa..00000000 --- a/src/notifications/PlaybackControlNotification.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "PlaybackControlNotification.h" - -PlaybackControlNotification::PlaybackControlNotification(PlaybackControlNotification::Action action, bool smoothTransition) - : _action(action) - , _smoothTransition(smoothTransition) -{ -} - -std::string PlaybackControlNotification::name() const -{ - return "PlaybackControlNotification"; -} - -PlaybackControlNotification::Action PlaybackControlNotification::ControlAction() const -{ - return _action; -} - -bool PlaybackControlNotification::SmoothTransition() const -{ - return _smoothTransition; -} diff --git a/src/notifications/Quit.cpp b/src/notifications/Quit.cpp new file mode 100644 index 00000000..0beb38ac --- /dev/null +++ b/src/notifications/Quit.cpp @@ -0,0 +1,10 @@ +#include "Quit.h" + +namespace Notification { + +std::string Quit::name() const +{ + return "QuitNotification"; +} + +} // namespace Notification diff --git a/src/notifications/QuitNotification.h b/src/notifications/Quit.h similarity index 64% rename from src/notifications/QuitNotification.h rename to src/notifications/Quit.h index 5286f312..cd6d4d8f 100644 --- a/src/notifications/QuitNotification.h +++ b/src/notifications/Quit.h @@ -2,11 +2,15 @@ #include +namespace Notification { + /** * @brief Informs the application that the user wants to quit. */ -class QuitNotification : public Poco::Notification +class Quit : public Poco::Notification { public: std::string name() const override; }; + +} // namespace Notification diff --git a/src/notifications/QuitNotification.cpp b/src/notifications/QuitNotification.cpp deleted file mode 100644 index 194deca4..00000000 --- a/src/notifications/QuitNotification.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "QuitNotification.h" - -std::string QuitNotification::name() const -{ - return "QuitNotification"; -} diff --git a/src/notifications/UpdateWindowTitle.cpp b/src/notifications/UpdateWindowTitle.cpp new file mode 100644 index 00000000..ff2cc1ab --- /dev/null +++ b/src/notifications/UpdateWindowTitle.cpp @@ -0,0 +1,10 @@ +#include "UpdateWindowTitle.h" + +namespace Notification { + +std::string UpdateWindowTitle::name() const +{ + return "UpdateWindowTitleNotification"; +} + +} // namespace Notification diff --git a/src/notifications/UpdateWindowTitleNotification.h b/src/notifications/UpdateWindowTitle.h similarity index 62% rename from src/notifications/UpdateWindowTitleNotification.h rename to src/notifications/UpdateWindowTitle.h index d6cb0a4d..d05b7af1 100644 --- a/src/notifications/UpdateWindowTitleNotification.h +++ b/src/notifications/UpdateWindowTitle.h @@ -2,11 +2,15 @@ #include +namespace Notification { + /** * @brief Informs the application that the window title should be updated. */ -class UpdateWindowTitleNotification : public Poco::Notification +class UpdateWindowTitle : public Poco::Notification { public: std::string name() const override; }; + +} // namespace Notification diff --git a/src/notifications/UpdateWindowTitleNotification.cpp b/src/notifications/UpdateWindowTitleNotification.cpp deleted file mode 100644 index 54d9f783..00000000 --- a/src/notifications/UpdateWindowTitleNotification.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "UpdateWindowTitleNotification.h" - -std::string UpdateWindowTitleNotification::name() const -{ - return "UpdateWindowTitleNotification"; -} From c58d3717a71a1b1f813cc50b808d254505335a90 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Wed, 9 Sep 2026 18:53:33 +0200 Subject: [PATCH 2/6] Move playlist management logic into its own wrapper class For now, the wrapper class still uses the projectM playlist library under the hood. Once the playlist editor and advanced playback features are implemented, all logic (path traversal, sorting, playback logic) will move to this application, no longer needing the projectM playlist library. --- src/CMakeLists.txt | 2 + src/PresetPlaylist.cpp | 396 ++++++++++++++++++++++++++ src/PresetPlaylist.h | 323 +++++++++++++++++++++ src/ProjectMWrapper.cpp | 100 +++---- src/ProjectMWrapper.h | 32 +-- src/RenderLoop.cpp | 31 +- src/RenderLoop.h | 1 - src/SDLRenderingWindow.cpp | 12 +- src/gui/PresetSelection.cpp | 7 +- src/notifications/CMakeLists.txt | 2 + src/notifications/PlaylistChanged.cpp | 15 + src/notifications/PlaylistChanged.h | 44 +++ 12 files changed, 870 insertions(+), 95 deletions(-) create mode 100644 src/PresetPlaylist.cpp create mode 100644 src/PresetPlaylist.h create mode 100644 src/notifications/PlaylistChanged.cpp create mode 100644 src/notifications/PlaylistChanged.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d4e5accf..febaa109 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,8 @@ add_executable(projectMSDL WIN32 MACOSX_BUNDLE SDLRenderingWindow.h main.cpp projectMSDL.rc + PresetPlaylist.cpp + PresetPlaylist.h ) string(TIMESTAMP BUILD_YEAR "%Y" UTC) diff --git a/src/PresetPlaylist.cpp b/src/PresetPlaylist.cpp new file mode 100644 index 00000000..7b367d29 --- /dev/null +++ b/src/PresetPlaylist.cpp @@ -0,0 +1,396 @@ +/** + * @file PresetPlaylist.cpp + * @brief + * + **/ + +#include "PresetPlaylist.h" + +#include "notifications/PlaylistChanged.h" +#include "notifications/UpdateWindowTitle.h" + +#include + +#include +#include + +POCO_IMPLEMENT_EXCEPTION(PlaylistEmptyException, Poco::Exception, "PlaylistEmptyException") + +PresetPlaylist::Item::Item(std::string path) + : _path(std::move(path)) +{ + poco_assert_dbg(!_path.empty()); + + if (!_path.empty()) + { + _presetName = Poco::Path(_path).getBaseName(); + } +} + +PresetPlaylist::PresetPlaylist() + : _projectMPlaylist(projectm_playlist_create(nullptr)) +{ + if (!_projectMPlaylist) + { + poco_error(_logger, "Failed to create the projectM preset playlist manager instance."); + throw std::runtime_error("projectM Playlist Manager initialization failed"); + } + + projectm_playlist_set_preset_switched_event_callback(_projectMPlaylist, &PresetPlaylist::PresetSwitchedEvent, this); +} +PresetPlaylist::~PresetPlaylist() +{ + poco_assert(_projectMPlaylist); + projectm_playlist_set_preset_switched_event_callback(_projectMPlaylist, nullptr, nullptr); + projectm_playlist_destroy(_projectMPlaylist); + _projectMPlaylist = nullptr; +} + +void PresetPlaylist::Connect(projectm* projectMHandle) +{ + projectm_playlist_connect(_projectMPlaylist, projectMHandle); +} + +void PresetPlaylist::Disconnect() +{ + projectm_playlist_connect(_projectMPlaylist, nullptr); +} + +void PresetPlaylist::CurrentIndex(ItemList::size_type index, bool hardcut) +{ + if (_items.empty()) + { + throw PlaylistEmptyException(); + } + + if (index >= _items.size()) + { + throw std::out_of_range("Playlist index out of range"); + } + + _currentIndex = index; + + poco_assert(_projectMPlaylist); + projectm_playlist_set_position(_projectMPlaylist, _currentIndex, hardcut); +} + +void PresetPlaylist::ShuffleEnabled(bool enabled) const +{ + poco_assert(_projectMPlaylist); + projectm_playlist_set_shuffle(_projectMPlaylist, enabled); +} + +bool PresetPlaylist::ShuffleEnabled() const +{ + poco_assert(_projectMPlaylist); + return projectm_playlist_get_shuffle(_projectMPlaylist); +} + +void PresetPlaylist::Next(bool hardCut) +{ + poco_assert(_projectMPlaylist); + projectm_playlist_set_shuffle(_projectMPlaylist, false); + projectm_playlist_play_next(_projectMPlaylist, hardCut); + projectm_playlist_set_shuffle(_projectMPlaylist, _shuffle); +} + +void PresetPlaylist::Previous(bool hardCut) +{ + poco_assert(_projectMPlaylist); + projectm_playlist_set_shuffle(_projectMPlaylist, false); + projectm_playlist_play_previous(_projectMPlaylist, hardCut); + projectm_playlist_set_shuffle(_projectMPlaylist, _shuffle); +} + +void PresetPlaylist::LastInHistory(bool hardCut) +{ + poco_assert(_projectMPlaylist); + projectm_playlist_play_last(_projectMPlaylist, hardCut); +} + +void PresetPlaylist::Random(bool hardCut) +{ + poco_assert(_projectMPlaylist); + projectm_playlist_set_shuffle(_projectMPlaylist, true); + projectm_playlist_play_next(_projectMPlaylist, hardCut); + projectm_playlist_set_shuffle(_projectMPlaylist, _shuffle); +} + +void PresetPlaylist::BeginBatchEdit() +{ + _isBatchEditing = true; +} + +void PresetPlaylist::EndBatchEdit() +{ + if (_isBatchEditing) + { + _isBatchEditing = false; + SendPlaylistChangedNotification(); + } +} + +bool PresetPlaylist::AddItem(const Item& item, bool allowDuplicates) +{ + poco_assert(_projectMPlaylist); + + if (projectm_playlist_add_preset(_projectMPlaylist, item.Path().c_str(), allowDuplicates)) + { + _items.push_back(item); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + + return true; + } + + return false; +} + +PresetPlaylist::ItemList::size_type PresetPlaylist::AddPath(const std::string& path, + const bool recurseSubdirs, + const bool allowDuplicates) +{ + poco_assert(_projectMPlaylist); + + const uint32_t oldPlaylistSize = projectm_playlist_size(_projectMPlaylist); + const uint32_t addedPresets = projectm_playlist_add_path(_projectMPlaylist, path.c_str(), recurseSubdirs, allowDuplicates); + + if (addedPresets > 0) + { + // For now, we gather the new items from the playlist manager and add them here. + const auto newItems = projectm_playlist_items(_projectMPlaylist, oldPlaylistSize, addedPresets); + auto curItem = newItems; + while (*curItem) + { + if (**curItem != '\0') + { + _items.emplace_back(std::string(*curItem)); + } + ++curItem; + } + projectm_playlist_free_string_array(newItems); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + } + + return addedPresets; +} + +bool PresetPlaylist::InsertItem(const Item& item, ItemList::size_type index, bool allowDuplicates) +{ + poco_assert(_projectMPlaylist); + + if (projectm_playlist_insert_preset(_projectMPlaylist, item.Path().c_str(), index, allowDuplicates)) + { + _items.insert(_items.begin() + static_cast(index), item); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + + return true; + } + + return false; +} + +PresetPlaylist::ItemList::size_type PresetPlaylist::InsertPath(const std::string& path, + const ItemList::size_type index, + const bool recurseSubdirs, + const bool allowDuplicates) +{ + poco_assert(_projectMPlaylist); + + const uint32_t addedPresets = projectm_playlist_insert_path(_projectMPlaylist, path.c_str(), index, recurseSubdirs, allowDuplicates); + + if (addedPresets > 0) + { + // For now, we gather the new items from the playlist manager and add them here. + const auto newItems = projectm_playlist_items(_projectMPlaylist, index, addedPresets); + auto curItem = newItems; + while (*curItem) + { + if (**curItem != '\0') + { + _items.emplace_back(std::string(*curItem)); + } + ++curItem; + } + projectm_playlist_free_string_array(newItems); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + } + + return addedPresets; +} + +bool PresetPlaylist::RemoveItem(const ItemList::size_type index) +{ + if (index >= _items.size()) + { + return false; + } + + poco_assert(_projectMPlaylist); + + if (!projectm_playlist_remove_preset(_projectMPlaylist, index)) + { + return false; + } + + _items.erase(_items.begin() + static_cast(index)); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + + return true; +} + +bool PresetPlaylist::RemoveItems(const ItemList::size_type index, ItemList::size_type count) +{ + if (index >= _items.size()) + { + return false; + } + + if (index + count >= _items.size()) + { + count = _items.size() - index; + } + + poco_assert(_projectMPlaylist); + + if (!projectm_playlist_remove_presets(_projectMPlaylist, index, count)) + { + return false; + } + + _items.erase(_items.begin() + static_cast(index), + _items.begin() + static_cast(index) + static_cast(count)); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } + + return true; +} + +void PresetPlaylist::Clear() +{ + poco_assert(_projectMPlaylist); + + if (_items.empty()) + { + return; + } + + _items.clear(); + projectm_playlist_clear(_projectMPlaylist); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } +} + +void PresetPlaylist::Sort(SortPredicate predicate, + SortOrder order) +{ + SortRange(0, _items.size(), predicate, order); +} + +void PresetPlaylist::SortRange(ItemList::size_type startIndex, + ItemList::size_type count, + SortPredicate predicate, + SortOrder order) +{ + poco_assert(_projectMPlaylist); + + if (_items.empty()) + { + return; + } + + projectm_playlist_sort_predicate prjmPredicate; + switch (predicate) + { + case SortPredicate::FullPath: + default: + prjmPredicate = SORT_PREDICATE_FULL_PATH; + + break; + case SortPredicate::PresetName: + prjmPredicate = SORT_PREDICATE_FILENAME_ONLY; + break; + } + + projectm_playlist_sort_order prjmOrder; + switch (order) + { + case SortOrder::Ascending: + default: + prjmOrder = SORT_ORDER_ASCENDING; + break; + case SortOrder::Descending: + prjmOrder = SORT_ORDER_DESCENDING; + break; + case SortOrder::Random: + // ToDo: Implement. + throw std::runtime_error("Sort order Random is not supported yet."); + break; + } + + projectm_playlist_sort(_projectMPlaylist, startIndex, count, prjmPredicate, prjmOrder); + + // We just resync all items from the projectM playlist for now. + auto playlistSize = projectm_playlist_size(_projectMPlaylist); + _items.clear(); + _items.reserve(playlistSize); + const auto newItems = projectm_playlist_items(_projectMPlaylist, 0, playlistSize); + auto curItem = newItems; + while (*curItem) + { + if (**curItem != '\0') + { + _items.emplace_back(std::string(*curItem)); + } + ++curItem; + } + projectm_playlist_free_string_array(newItems); + + if (!_isBatchEditing) + { + SendPlaylistChangedNotification(); + } +} + +void PresetPlaylist::PresetSwitchedEvent(bool /* isHardCut */, unsigned int index, void* context) +{ + auto that = static_cast(context); + + if (!that->Empty()) + { + that->_currentIndex = index; + poco_information_f1(that->_logger, "Displaying preset: %s", that->CurrentItem().PresetName()); + } + + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::UpdateWindowTitle); +} + +void PresetPlaylist::SendPlaylistChangedNotification() const +{ + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaylistChanged(*this)); +} diff --git a/src/PresetPlaylist.h b/src/PresetPlaylist.h new file mode 100644 index 00000000..79c1e671 --- /dev/null +++ b/src/PresetPlaylist.h @@ -0,0 +1,323 @@ +#pragma once + +#include +#include + +#include +#include + +POCO_DECLARE_EXCEPTION(, PlaylistEmptyException, Poco::Exception); + +struct projectm; +struct projectm_playlist; + +/** + * @brief Class representing a single preset playlist in the application. + * + * Keeps preset filenames (full path and parsed base name), last playlist position + * and other useful data. + * + * @note For now, this class just mirrors the contents of a projectM-managed playlist. + * Future versions will use this class to fully control preset playback as well, + * replacing the projectM playlist library. + */ +class PresetPlaylist +{ +public: + /** + * Playlist sorting predicate (field) + */ + enum class SortPredicate : uint8_t + { + FullPath, //!< Sorts by full preset path. + PresetName //!< Sorts using the preset name only (e.g. file name). + }; + + /** + * Playlist sort order. + */ + enum class SortOrder : uint8_t + { + Ascending, //!< Lexical ascending order. + Descending, //!< Lexical descending order. + Random //!< Random order. + }; + + /** + * @brief A single playlist item. + * Stored information about a single playlist item, such as the full path + * and the human-readable preset name. + */ + class Item + { + public: + Item() = delete; + + /** + * Creates a new playlist item. + * @param path The full path to the preset file. + */ + explicit Item(std::string path); + + /** + * Returns the full filesystem path of the playlist item. + * @return The full path to the preset file. + */ + [[nodiscard]] const std::string& Path() const + { + return _path; + } + + /** + * Sets a custom display name for the playlist item. + * @param name The new item name to be displayed. + */ + void PresetName(const std::string& name) + { + _presetName = name; + } + + /** + * Returns a human-readable name of the playlist item. + * @return The human-readable name of the preset. + */ + [[nodiscard]] const std::string& PresetName() const + { + return _presetName; + } + + private: + std::string _path; //!< The full filesystem path to the preset. + std::string _presetName; //!< The display name for the preset. + }; + + using ItemList = std::vector; //!< A list of preset items + + /** + * Creates an empty playlist. + */ + PresetPlaylist(); + + /** + * Destructor. + */ + virtual ~PresetPlaylist(); + + /** + * Connects the playlist with a projectM instance. + * @param projectMHandle The projectM instance handle to connect the playlist to. + */ + void Connect(projectm* projectMHandle); + + /** + * Disconnects the playlist from the projectM instance. + */ + void Disconnect(); + + /** + * Returns a reference to the stored list of presets. + * @return A reference to the preset list. + */ + [[nodiscard]] const ItemList& Items() const + { + return _items; + } + + /** + * Returns whether the current playlist is empty or not. + * @return true if the playlist is empty, false if it contains at least one element. + */ + [[nodiscard]] bool Empty() const + { + return _items.empty(); + } + + /** + * Returns the number of presets in thist playlist instance. + * @return The number of elements in the current playlist. + */ + [[nodiscard]] ItemList::size_type Size() const + { + return _items.size(); + } + + /** + * Returns a reference to the currently active playlist item. + * @throws PlaylistEmptyException Thrown if the playlist is empty and there can't be a current item. + * @throws std::out_of_range Thrown if the index is outside the valid range. + * @return A reference to the current playlist item. + */ + [[nodiscard]] const Item& CurrentItem() const + { + if (!_items.empty()) + { + return _items.at(_currentIndex); + } + + throw PlaylistEmptyException(); + } + + /** + * Sets a new playlist index as the currently active preset. + * @throws PlaylistEmptyException Thrown if the playlist is empty and there can't be a current item. + * @param index The new playlist index to set. + */ + void CurrentIndex(ItemList::size_type index, bool hardcut); + + /** + * Returns the index of the currently active playlist item. + * @throws PlaylistEmptyException Thrown if the playlist is empty and there can't be a current item. + * @return The current playlist index. + */ + [[nodiscard]] ItemList::size_type CurrentIndex() const + { + if (_items.empty()) + { + throw PlaylistEmptyException(); + } + + return _currentIndex; + } + + /** + * Sets the shuffle mode. + * @param enabled The new value of the shuffle flag + */ + void ShuffleEnabled(bool enabled) const; + + /** + * Returns whether shuffle mode is enabled or not. + * @return true if shuffle mode is enabled, false if not. + */ + [[nodiscard]] bool ShuffleEnabled() const; + + /** + * Plays the next item in the playlist, regardless of shuffle. + * @param hardCut If true, do not use a smooth transition. + */ + void Next(bool hardCut); + + /** + * Plays the previous item in the playlist, regardless of shuffle. + * @param hardCut If true, do not use a smooth transition. + */ + void Previous(bool hardCut); + + /** + * Plays the last item in the playlist's playback history. + * @param hardCut If true, do not use a smooth transition. + */ + void LastInHistory(bool hardCut); + + /** + * Plays a random item in the playlist, regardless of shuffle. + * @param hardCut If true, do not use a smooth transition. + */ + void Random(bool hardCut); + + /** + * @brief Enters batch edit mode, prevents sending change notifications. + * When using more than a single insert/remove call, always start batch mode and end + * it with EndBatchEdit() when done. + */ + void BeginBatchEdit(); + + /** + * @brief Exits batch edit mode and sends a change notification. + * @note The change notification is always sent, even if no changes were made when batch mode was active. + */ + void EndBatchEdit(); + + /** + * Adds a new item to the playlist. + * @param item The playlist item to add. + * @param allowDuplicates + * @return true if the item was added, false if it was a duplicate. + */ + bool AddItem(const Item& item, bool allowDuplicates); + + /** + * Adds preset files from a given path to the end of the playlist. + * @param path The path to scan for presets. + * @param recurseSubdirs true to recursively search subdirectories, fals to only use the current directory. + * @param allowDuplicates true to allow duplicate files (same physical path). + * @return The number of added presets. + */ + ItemList::size_type AddPath(const std::string& path, bool recurseSubdirs, bool allowDuplicates); + + /** + * Adds a new item to the playlist at the given index. + * @param item The playlist item to add. + * @param index The index to insert the item at. + * @param allowDuplicates true to allow duplicate files (same physical path). + * @return true if the item was added, false if it was a duplicate. + */ + bool InsertItem(const Item& item, ItemList::size_type index, bool allowDuplicates); + + /** + * Adds preset files from a given path at the given index position in the playlist. + * @param path The path to scan for presets. + * @param index The index to insert the new items at. + * @param recurseSubdirs true to recursively search subdirectories, fals to only use the current directory. + * @param allowDuplicates true to allow duplicate files (same physical path). + * @return The number of added presets. + */ + ItemList::size_type InsertPath(const std::string& path, ItemList::size_type index, bool recurseSubdirs, bool allowDuplicates); + + /** + * Removes a single item from the playlist. + * @param index The item index to remove. + */ + bool RemoveItem(ItemList::size_type index); + + /** + * Removes @a count number of items beginning at @a index. + * @param index The start index to remove items. + * @param count The number of items to remove. + */ + bool RemoveItems(ItemList::size_type index, ItemList::size_type count); + + /** + * Removes all items from the playlist. + */ + void Clear(); + + /** + * Sorts the whole playlist. + * @param predicate The predicate/field to sort by. + * @param order The sort order. + */ + void Sort(SortPredicate predicate, SortOrder order); + + /** + * Sorts a range of items in the current playlist. + * @param startIndex The playlist index to begin sorting at. + * @param count The nubmer of entries to sort beginning at @a index + * @param predicate The predicate/field to sort by. + * @param order The sort order. + */ + void SortRange(ItemList::size_type startIndex, ItemList::size_type count, SortPredicate predicate, SortOrder order); + +private: + /** + * @brief projectM callback. Called whenever a preset is switched in the current playlist. + * @param isHardCut True if the switch was a hard cut. + * @param index New preset playlist index. + * @param context Callback context, e.g. "this" pointer. + */ + static void PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context); + + /** + * Sends a notification that the playlist has changed. + */ + void SendPlaylistChangedNotification() const; + + projectm_playlist* _projectMPlaylist{nullptr}; //!< The projectM-managed playlist (temporary). + ItemList::size_type _currentIndex{}; //!< The currently active playlist index. + ItemList _items; //!< All presets in the current playlist + + bool _shuffle{false}; //!< Shuffle enabled + bool _isBatchEditing{false}; //!< Flag determining if the playlist is being batch-edited. + + Poco::Logger& _logger{Poco::Logger::get("PresetPlaylist")}; //!< The class logger. +}; diff --git a/src/ProjectMWrapper.cpp b/src/ProjectMWrapper.cpp index 1487c21d..328733cd 100644 --- a/src/ProjectMWrapper.cpp +++ b/src/ProjectMWrapper.cpp @@ -78,33 +78,28 @@ void ProjectMWrapper::initialize(Poco::Util::Application& app) } // Playlist - _playlist = projectm_playlist_create(_projectM); - if (!_playlist) - { - - poco_error(_logger, "Failed to create the projectM preset playlist manager instance."); - throw std::runtime_error("Playlist initialization failed"); - } - - projectm_playlist_set_shuffle(_playlist, _projectMConfigView->getBool("shuffleEnabled", true)); + _playlist = std::make_unique(); + _playlist->ShuffleEnabled(_projectMConfigView->getBool("shuffleEnabled", true)); + _playlist->BeginBatchEdit(); for (const auto& presetPath : presetPaths) { Poco::File file(presetPath); if (file.exists() && file.isFile()) { - projectm_playlist_add_preset(_playlist, presetPath.c_str(), false); + _playlist->AddItem(PresetPlaylist::Item(presetPath), false); } else { // Symbolic links also fall under this. Without complex resolving, we can't // be sure what the link exactly points to, especially if a trailing slash is missing. - projectm_playlist_add_path(_playlist, presetPath.c_str(), true, false); + _playlist->AddPath(presetPath, true, false); } } - projectm_playlist_sort(_playlist, 0, projectm_playlist_size(_playlist), SORT_PREDICATE_FILENAME_ONLY, SORT_ORDER_ASCENDING); + _playlist->Sort(PresetPlaylist::SortPredicate::PresetName, PresetPlaylist::SortOrder::Ascending); + _playlist->EndBatchEdit(); - projectm_playlist_set_preset_switched_event_callback(_playlist, &ProjectMWrapper::PresetSwitchedEvent, static_cast(this)); + _playlist->Connect(_projectM); } Poco::NotificationCenter::defaultCenter().addObserver(_playbackControlNotificationObserver); @@ -117,6 +112,8 @@ void ProjectMWrapper::initialize(Poco::Util::Application& app) void ProjectMWrapper::uninitialize() { + _playlist.reset(); + _userConfig->propertyRemoved -= Poco::delegate(this, &ProjectMWrapper::OnConfigurationPropertyRemoved); _userConfig->propertyChanged -= Poco::delegate(this, &ProjectMWrapper::OnConfigurationPropertyChanged); Poco::NotificationCenter::defaultCenter().removeObserver(_playbackControlNotificationObserver); @@ -127,12 +124,6 @@ void ProjectMWrapper::uninitialize() projectm_destroy(_projectM); _projectM = nullptr; } - - if (_playlist) - { - projectm_playlist_destroy(_playlist); - _playlist = nullptr; - } } projectm_handle ProjectMWrapper::ProjectM() const @@ -140,9 +131,24 @@ projectm_handle ProjectMWrapper::ProjectM() const return _projectM; } -projectm_playlist_handle ProjectMWrapper::Playlist() const +const PresetPlaylist& ProjectMWrapper::Playlist() const { - return _playlist; + if (_playlist) + { + return *_playlist; + } + + throw std::runtime_error("Playlist not available (ProjectMWrapper subsystem not yet initialized?)"); +} + +PresetPlaylist& ProjectMWrapper::Playlist() +{ + if (_playlist) + { + return *_playlist; + } + + throw std::runtime_error("Playlist not available (ProjectMWrapper subsystem not yet initialized?)"); } int ProjectMWrapper::TargetFPS() @@ -150,7 +156,7 @@ int ProjectMWrapper::TargetFPS() return _projectMConfigView->getInt("fps", 60); } -void ProjectMWrapper::UpdateRealFPS(float fps) +void ProjectMWrapper::UpdateRealFPS(float fps) const { projectm_set_fps(_projectM, static_cast(std::round(fps))); } @@ -174,6 +180,7 @@ void ProjectMWrapper::RenderFrame() Poco::ScopedLock lock(_audioBufferMutex); if (!_audioStagingBuffer.empty()) { + poco_assert(_audioStagingBuffer.size() % _audioChannels == 0); projectm_pcm_add_float(_projectM, _audioStagingBuffer.data(), _audioStagingBuffer.size() / _audioChannels, static_cast(_audioChannels)); _audioStagingBuffer.clear(); } @@ -184,20 +191,22 @@ void ProjectMWrapper::RenderFrame() void ProjectMWrapper::DisplayInitialPreset() { + poco_assert(_playlist); + if (!_projectMConfigView->getBool("enableSplash", true)) { if (_projectMConfigView->getBool("shuffleEnabled", true)) { - projectm_playlist_play_next(_playlist, true); + _playlist->Random(true); } else { - projectm_playlist_set_position(_playlist, 0, true); + _playlist->CurrentIndex(0, true); } } } -void ProjectMWrapper::ChangeBeatSensitivity(float value) +void ProjectMWrapper::ChangeBeatSensitivity(float value) const { projectm_set_beat_sensitivity(_projectM, projectm_get_beat_sensitivity(_projectM) + value); Poco::NotificationCenter::defaultCenter().postNotification( @@ -220,47 +229,38 @@ std::string ProjectMWrapper::ProjectMRuntimeVersion() void ProjectMWrapper::PresetFileNameToClipboard() const { - auto presetName = projectm_playlist_item(_playlist, projectm_playlist_get_position(_playlist)); - SDL_SetClipboardText(presetName); - projectm_playlist_free_string(presetName); -} - -void ProjectMWrapper::PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context) -{ - auto that = reinterpret_cast(context); - auto presetName = projectm_playlist_item(that->_playlist, index); - poco_information_f1(that->_logger, "Displaying preset: %s", std::string(presetName)); - projectm_playlist_free_string(presetName); - - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::UpdateWindowTitle); + if (_playlist && !_playlist->Empty()) + { + SDL_SetClipboardText(_playlist->CurrentItem().Path().c_str()); + } } void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) { - bool shuffleEnabled = projectm_playlist_get_shuffle(_playlist); + if (!_playlist) + { + return; + } + + bool shuffleEnabled = _playlist->ShuffleEnabled(); + bool hardCut = !notification->SmoothTransition(); switch (notification->ControlAction()) { case Notification::PlaybackControl::Action::NextPreset: - projectm_playlist_set_shuffle(_playlist, false); - projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + _playlist->Next(hardCut); break; case Notification::PlaybackControl::Action::PreviousPreset: - projectm_playlist_set_shuffle(_playlist, false); - projectm_playlist_play_previous(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + _playlist->Previous(hardCut); break; case Notification::PlaybackControl::Action::LastPreset: - projectm_playlist_play_last(_playlist, !notification->SmoothTransition()); + _playlist->LastInHistory(hardCut); break; case Notification::PlaybackControl::Action::RandomPreset: { - projectm_playlist_set_shuffle(_playlist, true); - projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + _playlist->Random(hardCut); break; } @@ -332,7 +332,7 @@ void ProjectMWrapper::OnConfigurationPropertyRemoved(const std::string& key) if (key == "projectM.shuffleEnabled") { - projectm_playlist_set_shuffle(_playlist, _projectMConfigView->getBool("shuffleEnabled", true)); + _playlist->ShuffleEnabled(_projectMConfigView->getBool("shuffleEnabled", true)); } if (key == "projectM.aspectCorrectionEnabled") diff --git a/src/ProjectMWrapper.h b/src/ProjectMWrapper.h index 109efdce..8af28286 100644 --- a/src/ProjectMWrapper.h +++ b/src/ProjectMWrapper.h @@ -1,10 +1,10 @@ #pragma once +#include "PresetPlaylist.h" #include "notifications/AudioDataAvailable.h" #include "notifications/PlaybackControl.h" #include -#include #include #include @@ -30,10 +30,16 @@ class ProjectMWrapper : public Poco::Util::Subsystem projectm_handle ProjectM() const; /** - * Returns the playlist handle. - * @return The plaslist handle. + * Returns the current playlist. + * @return The currently active preset playlist. */ - projectm_playlist_handle Playlist() const; + const PresetPlaylist& Playlist() const; + + /** + * Returns the current playlist. + * @return The currently active preset playlist. + */ + PresetPlaylist& Playlist(); /** * Renders a single projectM frame. @@ -50,7 +56,7 @@ class ProjectMWrapper : public Poco::Util::Subsystem * @brief Updates projectM with the current, actual FPS value. * @param fps The current FPS value. */ - void UpdateRealFPS(float fps); + void UpdateRealFPS(float fps) const; /** * @brief If splash is disabled, shows the initial preset. @@ -62,19 +68,19 @@ class ProjectMWrapper : public Poco::Util::Subsystem * @brief Changes beat sensitivity by the given value. * @param value A positive or negative delta value. */ - void ChangeBeatSensitivity(float value); + void ChangeBeatSensitivity(float value) const; /** * @brief Returns the libprojectM version this application was built against. * @return A string with the libprojectM build version. */ - std::string ProjectMBuildVersion(); + static std::string ProjectMBuildVersion(); /** * @brief Returns the libprojectM version this applications currently runs with. * @return A string with the libprojectM runtime library version. */ - std::string ProjectMRuntimeVersion(); + static std::string ProjectMRuntimeVersion(); /** * Copies the full path of the current preset into the OS clipboard. @@ -82,14 +88,6 @@ class ProjectMWrapper : public Poco::Util::Subsystem void PresetFileNameToClipboard() const; private: - /** - * @brief projectM callback. Called whenever a preset is switched. - * @param isHardCut True if the switch was a hard cut. - * @param index New preset playlist index. - * @param context Callback context, e.g. "this" pointer. - */ - static void PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context); - void PlaybackControlNotificationHandler(const Poco::AutoPtr& notification); void AudioDataAvailableNotificationHandler(const Poco::AutoPtr& notification); @@ -112,7 +110,7 @@ class ProjectMWrapper : public Poco::Util::Subsystem Poco::AutoPtr _projectMConfigView; //!< View of the "projectM" configuration subkey in the "effective" configuration. projectm_handle _projectM{nullptr}; //!< Pointer to the projectM instance used by the application. - projectm_playlist_handle _playlist{nullptr}; //!< Pointer to the projectM playlist manager instance. + std::unique_ptr _playlist; //!< The currently active playlist. uint32_t _audioChannels{0}; //!< Number of audio channels of the current capture device. std::vector _audioStagingBuffer; //!< Buffer which receives audio data from the capture implementation. diff --git a/src/RenderLoop.cpp b/src/RenderLoop.cpp index 083ed8b1..0325634d 100644 --- a/src/RenderLoop.cpp +++ b/src/RenderLoop.cpp @@ -17,7 +17,6 @@ RenderLoop::RenderLoop() , _projectMWrapper(Poco::Util::Application::instance().getSubsystem()) , _sdlRenderingWindow(Poco::Util::Application::instance().getSubsystem()) , _projectMHandle(_projectMWrapper.ProjectM()) - , _playlistHandle(_projectMWrapper.Playlist()) , _projectMGui(Poco::Util::Application::instance().getSubsystem()) , _userConfig(ProjectMSDLApplication::instance().UserConfiguration()) { @@ -53,8 +52,6 @@ void RenderLoop::Run() } notificationCenter.removeObserver(_quitNotificationObserver); - - projectm_playlist_set_preset_switched_event_callback(_playlistHandle, nullptr, nullptr); } void RenderLoop::PollEvents() @@ -115,16 +112,15 @@ void RenderLoop::PollEvents() bool skipToDropped = _userConfig->getBool("projectM.skipToDropped", true); bool droppedFolderOverride = _userConfig->getBool("projectM.droppedFolderOverride", false); - - bool shuffle = projectm_playlist_get_shuffle(_playlistHandle); + bool shuffle = _projectMWrapper.Playlist().ShuffleEnabled(); if (shuffle && skipToDropped) { // if shuffle is enabled, we disable it temporarily, so the dropped preset is played next // if skipToDropped is false, we also keep shuffle enabled, as it doesn't matter since the current preset is unaffected - projectm_playlist_set_shuffle(_playlistHandle, false); + _projectMWrapper.Playlist().ShuffleEnabled(false); } - int index = projectm_playlist_get_position(_playlistHandle) + 1; + auto insertIndex = _projectMWrapper.Playlist().CurrentIndex() + 1; do { @@ -141,11 +137,11 @@ void RenderLoop::PollEvents() break; // exit the block and go to the shuffle check } - if (projectm_playlist_insert_preset(_playlistHandle, droppedFilePath, index, true)) + if (_projectMWrapper.Playlist().InsertItem(PresetPlaylist::Item(droppedFileP.toString()), insertIndex, true)) { if (skipToDropped) { - projectm_playlist_play_next(_playlistHandle, true); + _projectMWrapper.Playlist().Next(true); } poco_information_f1(_logger, "Added preset: %s", std::string(droppedFilePath)); // no need to toast single presets, as its obvious if a preset was loaded. @@ -154,16 +150,17 @@ void RenderLoop::PollEvents() else { // handle dropped directory + _projectMWrapper.Playlist().BeginBatchEdit(); // if droppedFolderOverride is enabled, we clear the playlist first // current edge case: if the dropped directory is invalid or contains no presets, then it still clears the playlist if (droppedFolderOverride) { - projectm_playlist_clear(_playlistHandle); - index = 0; + _projectMWrapper.Playlist().Clear(); + insertIndex = 0; } - uint32_t addedFilesCount = projectm_playlist_insert_path(_playlistHandle, droppedFilePath, index, true, true); + uint32_t addedFilesCount = _projectMWrapper.Playlist().InsertPath(droppedFilePath, insertIndex, true, true); if (addedFilesCount > 0) { std::string toastMessage = "Added " + std::to_string(addedFilesCount) + " presets from " + droppedFilePath; @@ -171,7 +168,7 @@ void RenderLoop::PollEvents() if (skipToDropped || droppedFolderOverride) { // if skip to dropped is true, or if a folder was dropped and it overrode the playlist, we skip to the next preset - projectm_playlist_play_next(_playlistHandle, true); + _projectMWrapper.Playlist().Next(true); } Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); } @@ -181,12 +178,14 @@ void RenderLoop::PollEvents() Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); poco_information_f1(_logger, "%s", toastMessage); } + + _projectMWrapper.Playlist().EndBatchEdit(); } } while (false); if (shuffle && skipToDropped) { - projectm_playlist_set_shuffle(_playlistHandle, true); + _projectMWrapper.Playlist().ShuffleEnabled(true); } SDL_free(droppedFilePath); @@ -360,12 +359,12 @@ void RenderLoop::ScrollEvent(const SDL_MouseWheelEvent& event) // Wheel up is positive if (event.y > 0) { - projectm_playlist_play_next(_playlistHandle, true); + _projectMWrapper.Playlist().Next(true); } // Wheel down is negative else if (event.y < 0) { - projectm_playlist_play_previous(_playlistHandle, true); + _projectMWrapper.Playlist().Previous(true); } } diff --git a/src/RenderLoop.h b/src/RenderLoop.h index e62f579c..12df785a 100644 --- a/src/RenderLoop.h +++ b/src/RenderLoop.h @@ -77,7 +77,6 @@ class RenderLoop SDLRenderingWindow& _sdlRenderingWindow; projectm_handle _projectMHandle{nullptr}; - projectm_playlist_handle _playlistHandle{nullptr}; ProjectMGUI& _projectMGui; diff --git a/src/SDLRenderingWindow.cpp b/src/SDLRenderingWindow.cpp index d9273ed6..5caa0729 100644 --- a/src/SDLRenderingWindow.cpp +++ b/src/SDLRenderingWindow.cpp @@ -398,14 +398,14 @@ void SDLRenderingWindow::UpdateWindowTitle() auto& app = Poco::Util::Application::instance(); auto& projectMWrapper = app.getSubsystem(); - auto presetName = projectm_playlist_item(projectMWrapper.Playlist(), projectm_playlist_get_position(projectMWrapper.Playlist())); - - if (presetName) + if (!projectMWrapper.Playlist().Empty()) { - Poco::Path presetFile(presetName); - projectm_playlist_free_string(presetName); + const auto& currentItem = projectMWrapper.Playlist().CurrentItem(); - newTitle += " ➫ " + presetFile.getBaseName(); + if (!currentItem.PresetName().empty()) + { + newTitle += " ➫ " + currentItem.PresetName(); + } } if (projectm_get_preset_locked(projectMWrapper.ProjectM())) diff --git a/src/gui/PresetSelection.cpp b/src/gui/PresetSelection.cpp index 7ecb6c57..7d049d64 100644 --- a/src/gui/PresetSelection.cpp +++ b/src/gui/PresetSelection.cpp @@ -29,7 +29,7 @@ void PresetSelection::Draw() if (ImGui::SliderInt("Playlist Position", reinterpret_cast(&_playlistPosition), 0, _playlistSize - 1)) { auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - projectm_playlist_set_position(projectMWrapper.Playlist(), _playlistPosition, true); + projectMWrapper.Playlist().CurrentIndex(_playlistPosition, true); } ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); @@ -45,10 +45,7 @@ void PresetSelection::Draw() if (ImGui::Button("Random Preset")) { auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - bool shuffleEnabled = projectm_playlist_get_shuffle(projectMWrapper.Playlist()); - projectm_playlist_set_shuffle(projectMWrapper.Playlist(), true); - projectm_playlist_play_next(projectMWrapper.Playlist(), true); - projectm_playlist_set_shuffle(projectMWrapper.Playlist(), shuffleEnabled); + projectMWrapper.Playlist().Random(true); } ImGui::SameLine(); diff --git a/src/notifications/CMakeLists.txt b/src/notifications/CMakeLists.txt index e85ec145..2493baf3 100644 --- a/src/notifications/CMakeLists.txt +++ b/src/notifications/CMakeLists.txt @@ -5,6 +5,8 @@ add_library(ProjectMSDL-Notifications STATIC DisplayToast.h PlaybackControl.cpp PlaybackControl.h + PlaylistChanged.cpp + PlaylistChanged.h Quit.cpp Quit.h UpdateWindowTitle.cpp diff --git a/src/notifications/PlaylistChanged.cpp b/src/notifications/PlaylistChanged.cpp new file mode 100644 index 00000000..50425901 --- /dev/null +++ b/src/notifications/PlaylistChanged.cpp @@ -0,0 +1,15 @@ +#include "PlaylistChanged.h" + +namespace Notification { + +std::string PlaylistChanged::name() const +{ + return "PlaylistChangedNotification"; +} + +PlaylistChanged::PlaylistChanged(const PresetPlaylist& playlist) + : _playlist(playlist) +{ +} + +} // namespace Notification diff --git a/src/notifications/PlaylistChanged.h b/src/notifications/PlaylistChanged.h new file mode 100644 index 00000000..3d503c8e --- /dev/null +++ b/src/notifications/PlaylistChanged.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +class PresetPlaylist; + +namespace Notification { + +/** + * @brief Notifies observers that a playlist has been changed. + * + * This notification will be sent each time at least one playlist item changes + * in some way, e.g. new item added, item removed or if the playlist has been fully replaced. + * This allows to cache items/filenames in dialogs etc. without the need to query those each frame + * from the playlist manager. + * + * This notification will be sent by any playlist object, so the subscriber has to decide + * if it's the correct one. + */ +class PlaylistChanged : public Poco::Notification +{ +public: + std::string name() const override; + + PlaylistChanged() = delete; + + explicit PlaylistChanged(const PresetPlaylist& playlist); + + /** + * Returns a reference the to playlist which was changed. + * @note Don't store the reference, as the playlist passed in the notification + * might be a temporary object! + * @return A reference the to playlist which was changed. + */ + const PresetPlaylist& Playlist() const + { + return _playlist; + } + +private: + const PresetPlaylist& _playlist; +}; + +} // namespace Notification From 8ed3c5d2874561554981101e49292a983b0ccfad Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Wed, 9 Sep 2026 18:54:34 +0200 Subject: [PATCH 3/6] Delete unused PresetSelection class --- src/gui/CMakeLists.txt | 2 -- src/gui/PresetSelection.cpp | 67 ------------------------------------- src/gui/PresetSelection.h | 27 --------------- 3 files changed, 96 deletions(-) delete mode 100644 src/gui/PresetSelection.cpp delete mode 100644 src/gui/PresetSelection.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a2c72ee3..79deaaf5 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -29,8 +29,6 @@ add_library(ProjectMSDL-GUI STATIC HelpWindow.h MainMenu.cpp MainMenu.h - PresetSelection.cpp - PresetSelection.h ProjectMGUI.cpp ProjectMGUI.h SettingsWindow.cpp diff --git a/src/gui/PresetSelection.cpp b/src/gui/PresetSelection.cpp deleted file mode 100644 index 7d049d64..00000000 --- a/src/gui/PresetSelection.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "PresetSelection.h" - -#include "ProjectMWrapper.h" -#include "SDLRenderingWindow.h" - -#include "imgui.h" - -#include - -void PresetSelection::Show() -{ - _visible = true; -} - -void PresetSelection::Draw() -{ - if (ImGui::Begin("projectM Settings", &_visible)) - { - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Hint:"); - ImGui::SameLine(); - ImGui::Text("Press after clicking on a slider to enter a custom value."); - - if (ImGui::SliderFloat("Preset Display Duration", &_displayDuration, 1.0f, 60.0f)) - { - auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - projectm_set_preset_duration(projectMWrapper.ProjectM(), _displayDuration); - } - - if (ImGui::SliderInt("Playlist Position", reinterpret_cast(&_playlistPosition), 0, _playlistSize - 1)) - { - auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - projectMWrapper.Playlist().CurrentIndex(_playlistPosition, true); - } - - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate); - - if (ImGui::Button("Toggle Fullscreen")) - { - auto& renderingWindow = Poco::Util::Application::instance().getSubsystem(); - renderingWindow.ToggleFullscreen(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Random Preset")) - { - auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - projectMWrapper.Playlist().Random(true); - } - - ImGui::SameLine(); - - if (ImGui::Button("Lock Preset")) - { - auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); - projectm_set_preset_locked(projectMWrapper.ProjectM(), !projectm_get_preset_locked(projectMWrapper.ProjectM())); - } - - ImGui::SameLine(); - - if (ImGui::Button("Load Preset...")) - { - _fileChooser.Show(); - } - } - ImGui::End(); -} \ No newline at end of file diff --git a/src/gui/PresetSelection.h b/src/gui/PresetSelection.h deleted file mode 100644 index 04b0bba9..00000000 --- a/src/gui/PresetSelection.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "FileChooser.h" - -#include - -#include - -class PresetSelection -{ -public: - - void Show(); - - void Draw(); - -protected: - FileChooser _fileChooser; //!< File chooser dialog. - - bool _visible{ true }; //!< Flag for settings window visibility. - float _displayDuration{ 0.0f }; //!< Preset display time - uint32_t _playlistPosition{ 0 }; //!< Playlist position - uint32_t _playlistSize{ 0 }; //!< Playlist size - - Poco::Logger& _logger{ Poco::Logger::get("PresetSelection") }; //!< The class logger. - -}; From 6bc6b301d3a0e20e12a4fe4c68f29b41e5bf7a28 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Wed, 9 Sep 2026 19:13:30 +0200 Subject: [PATCH 4/6] Add ImGui's stdlib TextInput wrapper and use it in FileChooser --- ImGui.cmake | 3 +++ src/gui/FileChooser.cpp | 8 +++----- src/gui/FileChooser.h | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ImGui.cmake b/ImGui.cmake index 74d48d9e..263619eb 100644 --- a/ImGui.cmake +++ b/ImGui.cmake @@ -10,6 +10,8 @@ add_library(ImGui STATIC vendor/imgui/backends/imgui_impl_opengl3.cpp vendor/imgui/backends/imgui_impl_opengl3.h vendor/imgui/backends/imgui_impl_opengl3_loader.h + vendor/imgui/misc/cpp/imgui_stdlib.cpp + vendor/imgui/misc/cpp/imgui_stdlib.h ) target_link_libraries(ImGui @@ -39,6 +41,7 @@ target_include_directories(ImGui PUBLIC ${CMAKE_SOURCE_DIR}/vendor/imgui ${CMAKE_SOURCE_DIR}/vendor/imgui/backends + ${CMAKE_SOURCE_DIR}/vendor/imgui/misc/cpp ${SDL2_INCLUDE_DIRS} ) diff --git a/src/gui/FileChooser.cpp b/src/gui/FileChooser.cpp index dd1956e7..41fc2aa9 100644 --- a/src/gui/FileChooser.cpp +++ b/src/gui/FileChooser.cpp @@ -1,6 +1,7 @@ #include "FileChooser.h" #include "imgui.h" +#include "imgui_stdlib.h" #include #include @@ -105,14 +106,11 @@ bool FileChooser::Draw() ImGui::Separator(); - char pathBuffer[2048]{}; - strncpy(pathBuffer, _currentDir.toString().c_str(), std::min(2047, _currentDir.toString().size())); - ImGui::SetNextItemWidth(-1); - if (ImGui::InputText("##path", &pathBuffer[0], IM_ARRAYSIZE(pathBuffer)), ImGuiInputTextFlags_EnterReturnsTrue) + if (ImGui::InputText("##path", &_currentDirString, ImGuiInputTextFlags_EnterReturnsTrue)) { - ChangeDirectory(std::string(pathBuffer)); + ChangeDirectory(_currentDirString); } if (ImGui::BeginListBox("##filelist", ImVec2(-1, -ImGui::GetTextLineHeight() - ImGui::GetStyle().FramePadding.y * 2 - 4))) diff --git a/src/gui/FileChooser.h b/src/gui/FileChooser.h index 5469d3a8..3c548623 100644 --- a/src/gui/FileChooser.h +++ b/src/gui/FileChooser.h @@ -153,6 +153,7 @@ class FileChooser bool _showHidden{ false }; //!< If true, hidden files/dirs are shown. bool _multiSelect{ false }; //!< If true, selecting multiple files/directories is allowed. Poco::Path _currentDir{ Poco::Path::current() }; //!< Current working dir. + std::string _currentDirString{ _currentDir.toString() }; //!< Current working dir as string for editing. std::vector _currentFileList; //!< File list of current directory std::vector _selectedFiles; //!< Currently selected file(s). int _selectedFileIndex{ 0 }; //!< Last selected item in the file list. From 6027c3b25d4e3e2c01fe4dad182214ec1b6ed0c5 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 10 Sep 2026 01:09:36 +0200 Subject: [PATCH 5/6] Implement Preset Chooser Window --- src/RenderLoop.cpp | 1 + src/gui/CMakeLists.txt | 2 + src/gui/PresetChooser.cpp | 427 ++++++++++++++++++++++++++++++++++++++ src/gui/PresetChooser.h | 66 ++++++ src/gui/ProjectMGUI.cpp | 30 ++- src/gui/ProjectMGUI.h | 12 ++ 6 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 src/gui/PresetChooser.cpp create mode 100644 src/gui/PresetChooser.h diff --git a/src/RenderLoop.cpp b/src/RenderLoop.cpp index 0325634d..537bb2bb 100644 --- a/src/RenderLoop.cpp +++ b/src/RenderLoop.cpp @@ -308,6 +308,7 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) _sdlRenderingWindow.NextDisplay(); break; } + _projectMGui.ShowPresetChooser(); break; case SDLK_n: diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 79deaaf5..a9989b97 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -29,6 +29,8 @@ add_library(ProjectMSDL-GUI STATIC HelpWindow.h MainMenu.cpp MainMenu.h + PresetChooser.cpp + PresetChooser.h ProjectMGUI.cpp ProjectMGUI.h SettingsWindow.cpp diff --git a/src/gui/PresetChooser.cpp b/src/gui/PresetChooser.cpp new file mode 100644 index 00000000..3776bfa9 --- /dev/null +++ b/src/gui/PresetChooser.cpp @@ -0,0 +1,427 @@ +#include "PresetChooser.h" + +#include "ProjectMWrapper.h" + +#include +#include + +#include +#include + +#include + +#include + +PresetChooser::PresetChooser(PresetPlaylist::ItemList currentPlaylistItems, + const PresetPlaylist::ItemList::size_type initialSelectionIndex) + : _playlistItems(std::move(currentPlaylistItems)) + , _selectedIndex(initialSelectionIndex) +{ +} + +bool PresetChooser::Draw() +{ + std::string windowId = "Preset Quick Search###PresetChooser"; + + if (!_isOpen) + { + _isOpen = true; + // Immediately scroll to the current playlist index + _selectionChanged = true; + + // Skip this frame so ImGui doesn't add the hotkey to the text field input buffer + return true; + } + + if (_firstDraw) + { + ImGui::OpenPopup(windowId.c_str()); + } + + ImGui::SetNextWindowSize(ImVec2(1050, 0), ImGuiCond_FirstUseEver); + if (ImGui::BeginPopupModal(windowId.c_str(), &_isOpen, ImGuiWindowFlags_NoCollapse)) + { + ImGui::PushStyleColor(ImGuiCol_Text, 0x80808080); + ImGui::Text("Use Up/Down/PgUp/PgDown/Home/End to navigate, ENTER to select and ESC to exit"); + ImGui::PopStyleColor(); + + ImGui::SetNextItemWidth(-1); + if (_firstDraw) + { + ImGui::SetKeyboardFocusHere(0); + } + ImGui::InputText("##FilterText", &_searchPhrase, ImGuiInputTextFlags_None); + + ImGui::Separator(); + + float listItemHeight{}; + float listHeight{}; + + bool listIsFiltered = UpdateFilteredList(); + if (listIsFiltered) + { + DrawFilteredItems(listItemHeight, listHeight); + } + else + { + DrawAllItems(listItemHeight, listHeight); + } + + int itemsPerPage{}; + if (listItemHeight > 0 && listHeight > 0) + { + itemsPerPage = static_cast(listHeight / listItemHeight); + } + + // Navigation + if (listIsFiltered) + { + NavigateFilteredItems(itemsPerPage); + } + else + { + NavigateAllItems(itemsPerPage); + } + + if (ImGui::IsKeyPressed(ImGuiKey_Enter)) + { + PlayPresetIndex(_selectedIndex); + ImGui::CloseCurrentPopup(); + _isOpen = false; + } + + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) + { + ImGui::CloseCurrentPopup(); + _isOpen = false; + } + + ImGui::EndPopup(); + } + else + { + ImGui::CloseCurrentPopup(); + _isOpen = false; + } + + _firstDraw = false; + + return _isOpen; +} + +void PresetChooser::NavigateAllItems(const int itemsPerPage) +{ + if (_playlistItems.empty()) + { + return; + } + + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + { + if (_selectedIndex > 0) + { + --_selectedIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + { + if (_selectedIndex < _playlistItems.size() - 1) + { + ++_selectedIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) + { + if (_selectedIndex > 0) + { + if (_selectedIndex > itemsPerPage) + { + _selectedIndex -= itemsPerPage; + } + else + { + _selectedIndex = 0; + } + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) + { + if (_selectedIndex < _playlistItems.size() - 1) + { + if (_selectedIndex + itemsPerPage < _playlistItems.size() - 1) + { + _selectedIndex += itemsPerPage; + } + else + { + _selectedIndex = _playlistItems.size() - 1; + } + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_Home)) + { + _selectedIndex = 0; + _selectionChanged = true; + } + + if (ImGui::IsKeyPressed(ImGuiKey_End)) + { + _selectedIndex = _playlistItems.size() - 1; + _selectionChanged = true; + } +} + +void PresetChooser::NavigateFilteredItems(int itemsPerPage) +{ + if (_playlistItemsFiltered.empty()) + { + return; + } + + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + { + if (_selectedFilteredIndex > 0) + { + --_selectedFilteredIndex; + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + { + if (_selectedFilteredIndex < _playlistItemsFiltered.size() - 1) + { + ++_selectedFilteredIndex; + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) + { + if (_selectedFilteredIndex > 0) + { + if (_selectedFilteredIndex > itemsPerPage) + { + _selectedFilteredIndex -= itemsPerPage; + } + else + { + _selectedFilteredIndex = 0; + } + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) + { + if (_selectedFilteredIndex < _playlistItemsFiltered.size() - 1) + { + if (_selectedFilteredIndex + itemsPerPage < _playlistItemsFiltered.size() - 1) + { + _selectedFilteredIndex += itemsPerPage; + } + else + { + _selectedFilteredIndex = _playlistItemsFiltered.size() - 1; + } + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_Home)) + { + _selectedFilteredIndex = 0; + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } + + if (ImGui::IsKeyPressed(ImGuiKey_End)) + { + _selectedFilteredIndex = _playlistItemsFiltered.size() - 1; + _selectedIndex = _playlistItemsFiltered.at(_selectedFilteredIndex).itemIndex; + _selectionChanged = true; + } +} + +bool PresetChooser::UpdateFilteredList() +{ + if (_searchPhrase == _filterPhrase) + { + return !_filterPhrase.empty(); + } + + // Filter items word by word and case-insensitively to allow more flexible searches + Poco::StringTokenizer tok(Poco::toLower(_searchPhrase), " ", Poco::StringTokenizer::TOK_IGNORE_EMPTY); + + _playlistItemsFiltered.clear(); + if (tok.count() == 0) + { + _filterPhrase = ""; + return false; + } + + _filterPhrase = _searchPhrase; + + bool selectedIndexInFilteredList{false}; + size_t index{}; + for (const auto& item : _playlistItems) + { + std::string lowerCaseName = Poco::toLower(item.PresetName()); + if (std::all_of(tok.begin(), tok.end(), + [&](const std::string& word) { + return lowerCaseName.find(word) != std::string::npos; + })) + { + if (index == _selectedIndex) + { + _selectedFilteredIndex = _playlistItemsFiltered.size(); + selectedIndexInFilteredList = true; + } + _playlistItemsFiltered.emplace_back(item, index); + } + ++index; + } + + if (!selectedIndexInFilteredList) + { + _selectedFilteredIndex = 0; + if (!_playlistItemsFiltered.empty()) + { + _selectedIndex = _playlistItemsFiltered.at(0).itemIndex; + } + else + { + _selectedIndex = 0; + } + } + + _selectionChanged = true; + + return true; +} + +void PresetChooser::DrawAllItems(float& listItemHeight, float& listHeight) +{ + if (ImGui::BeginListBox("##FilteredPresets", ImVec2(-1, -ImGui::GetTextLineHeight() - ImGui::GetStyle().FramePadding.y * 2 - 4))) + { + ImGuiListClipper clipper; + clipper.Begin(_playlistItems.size()); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + std::string label = _playlistItems.at(i).PresetName() + "##" + std::to_string(i); + bool popColor = false; + if (i == _selectedIndex) + { + ImGui::PushStyleColor(ImGuiCol_Text, 0xFF00FF80); + popColor = true; + } + if (ImGui::Selectable(label.c_str(), i == _selectedIndex, ImGuiSelectableFlags_AllowDoubleClick)) + { + if (ImGui::IsMouseDoubleClicked(0)) + { + PlayPresetIndex(i); + ImGui::CloseCurrentPopup(); + _isOpen = false; + } + else + { + _selectedIndex = i; + } + } + if (popColor) + { + ImGui::PopStyleColor(); + } + } + } + + listItemHeight = clipper.ItemsHeight; + + if (_selectionChanged) + { + float itemPosY = clipper.StartPosY + listItemHeight * _selectedIndex; + ImGui::SetScrollFromPosY(itemPosY - ImGui::GetWindowPos().y); + _selectionChanged = false; + } + + ImGui::EndListBox(); + } + + listHeight = ImGui::GetItemRectSize().y; +} + +void PresetChooser::DrawFilteredItems(float& listItemHeight, float& listHeight) +{ + // _selectedIndex always points to the original playlist index, so we can keep the selection + // while changing the filter. + if (ImGui::BeginListBox("##FilteredPresets", ImVec2(-1, -ImGui::GetTextLineHeight() - ImGui::GetStyle().FramePadding.y * 2 - 4))) + { + ImGuiListClipper clipper; + clipper.Begin(static_cast(_playlistItemsFiltered.size())); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + std::string label = _playlistItemsFiltered.at(i).item.PresetName() + "##" + std::to_string(i); + bool popColor = false; + if (i == _selectedFilteredIndex) + { + ImGui::PushStyleColor(ImGuiCol_Text, 0xFF00FF80); + popColor = true; + } + if (ImGui::Selectable(label.c_str(), i == _selectedFilteredIndex, ImGuiSelectableFlags_AllowDoubleClick)) + { + if (ImGui::IsMouseDoubleClicked(0)) + { + PlayPresetIndex(_playlistItemsFiltered.at(i).itemIndex); + ImGui::CloseCurrentPopup(); + _isOpen = false; + } + else + { + _selectedFilteredIndex = i; + _selectedIndex = _playlistItemsFiltered.at(i).itemIndex; + } + } + if (popColor) + { + ImGui::PopStyleColor(); + } + } + } + + listItemHeight = clipper.ItemsHeight; + + if (_selectionChanged) + { + float itemPosY = clipper.StartPosY + listItemHeight * _selectedFilteredIndex; + ImGui::SetScrollFromPosY(itemPosY - ImGui::GetWindowPos().y); + _selectionChanged = false; + } + + ImGui::EndListBox(); + } + + listHeight = ImGui::GetItemRectSize().y; +} + +void PresetChooser::PlayPresetIndex(PresetPlaylist::ItemList::size_type index) +{ + bool hardCut = !ImGui::GetIO().KeyShift; + auto& projectMWrapper = Poco::Util::Application::instance().getSubsystem(); + projectMWrapper.Playlist().CurrentIndex(index, hardCut); +} diff --git a/src/gui/PresetChooser.h b/src/gui/PresetChooser.h new file mode 100644 index 00000000..e4d7e5f0 --- /dev/null +++ b/src/gui/PresetChooser.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "PresetPlaylist.h" + +class ProjectMGUI; + +/** + * @class PresetChooser + * @brief Displays a simple search window to quickly filter and select a preset from the current playlist. + * + * This UI window will be displayed if the menu bar isn't open and allows + * to quickly filter and search the current playlist. + */ +class PresetChooser +{ +public: + PresetChooser() = delete; + + explicit PresetChooser(PresetPlaylist::ItemList currentPlaylistItems, + PresetPlaylist::ItemList::size_type initialSelectionIndex); + + virtual ~PresetChooser() = default; + + /** + * @brief Draws the preset chooser window. + */ + bool Draw(); + +private: + struct ItemWithIndex { + explicit ItemWithIndex(PresetPlaylist::Item item, const PresetPlaylist::ItemList::size_type itemIndex) + : item(std::move(item)) + , itemIndex(itemIndex) + { + } + PresetPlaylist::Item item; + PresetPlaylist::ItemList::size_type itemIndex{}; + }; + + void NavigateAllItems(int itemsPerPage); + + void NavigateFilteredItems(int itemsPerPage); + + bool UpdateFilteredList(); + + void DrawAllItems(float& listItemHeight, float& listHeight); + + void DrawFilteredItems(float& listItemHeight, float& listHeight); + + void PlayPresetIndex(PresetPlaylist::ItemList::size_type index); + + std::string _searchPhrase; //!< The currently entered search phrase. + std::string _filterPhrase; //!< The search phrase used for the previous filtering. + + PresetPlaylist::ItemList _playlistItems; //!< Cached playlist items + std::vector _playlistItemsFiltered; //!< Filtered playlist items, mapped to their original playlist index + + PresetPlaylist::ItemList::size_type _selectedIndex{}; + PresetPlaylist::ItemList::size_type _selectedFilteredIndex{}; + bool _selectionChanged{false}; + + bool _isOpen{false}; //!< Becomes true if the popup has been opened. + bool _firstDraw{true}; //!< true if the chooser hasn't been fully drawn yet. +}; diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 92c3e9a5..1fbced77 100644 --- a/src/gui/ProjectMGUI.cpp +++ b/src/gui/ProjectMGUI.cpp @@ -123,8 +123,13 @@ bool ProjectMGUI::Visible() const void ProjectMGUI::Draw() { + // Reset key/mouse capture flags in case ImGui enabled them in the previous frame + auto& io = ImGui::GetIO(); + io.WantCaptureKeyboard = false; + io.WantCaptureMouse = false; + // Don't render UI at all if there's no need. - if (!_toast && !_visible) + if (!_toast && !_visible && !_presetChooser) { return; } @@ -158,6 +163,14 @@ void ProjectMGUI::Draw() } } + if (_presetChooser) + { + if (!_presetChooser->Draw()) + { + _presetChooser.reset(); + } + } + if (_visible) { _mainMenu.Draw(); @@ -212,6 +225,21 @@ void ProjectMGUI::ShowHelpWindow() _helpWindow.Show(); } +void ProjectMGUI::ShowPresetChooser() +{ + if (_presetChooser) + { + return; + } + + _presetChooser = std::make_unique(_projectMWrapper->Playlist().Items(), _projectMWrapper->Playlist().CurrentIndex()); +} + +void ProjectMGUI::ClosePresetChooser() +{ + _presetChooser.reset(); +} + float ProjectMGUI::GetScalingFactor() { int windowWidth; diff --git a/src/gui/ProjectMGUI.h b/src/gui/ProjectMGUI.h index 3d04478b..ae815997 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -3,6 +3,7 @@ #include "AboutWindow.h" #include "HelpWindow.h" #include "MainMenu.h" +#include "PresetChooser.h" #include "SettingsWindow.h" #include "ToastMessage.h" @@ -104,6 +105,16 @@ class ProjectMGUI : public Poco::Util::Subsystem */ void ShowHelpWindow(); + /** + * @brief Displays the preset chooser window. + */ + void ShowPresetChooser(); + + /** + * @brief Closes the preset chooser window. + */ + void ClosePresetChooser(); + private: float GetScalingFactor(); @@ -131,6 +142,7 @@ class ProjectMGUI : public Poco::Util::Subsystem SettingsWindow _settingsWindow{*this}; //!< The settings window. AboutWindow _aboutWindow{*this}; //!< The about window. HelpWindow _helpWindow; //!< Help window with shortcuts and tips. + std::unique_ptr _presetChooser; //!< Preset quick search/filter popup. std::unique_ptr _toast; //!< Current toast to be displayed. From 6310b31b0abb57023adfdc62a37f0c031adebb95 Mon Sep 17 00:00:00 2001 From: Kai Blaschke Date: Thu, 10 Sep 2026 21:17:36 +0200 Subject: [PATCH 6/6] Clean up event handlers in RenderLoop --- src/RenderLoop.cpp | 260 +++++++++++++++++++++++++++------------------ src/RenderLoop.h | 9 +- 2 files changed, 163 insertions(+), 106 deletions(-) diff --git a/src/RenderLoop.cpp b/src/RenderLoop.cpp index 537bb2bb..9dc58d81 100644 --- a/src/RenderLoop.cpp +++ b/src/RenderLoop.cpp @@ -104,91 +104,7 @@ void RenderLoop::PollEvents() break; case SDL_DROPFILE: { - char* droppedFilePath = event.drop.file; - - // first we want to get the config settings that are relevant ehre - // namely skipToDropped and droppedFolderOverride - // we can get them from the projectMWrapper, in the _projectMConfigView available on it - bool skipToDropped = _userConfig->getBool("projectM.skipToDropped", true); - bool droppedFolderOverride = _userConfig->getBool("projectM.droppedFolderOverride", false); - - bool shuffle = _projectMWrapper.Playlist().ShuffleEnabled(); - if (shuffle && skipToDropped) - { - // if shuffle is enabled, we disable it temporarily, so the dropped preset is played next - // if skipToDropped is false, we also keep shuffle enabled, as it doesn't matter since the current preset is unaffected - _projectMWrapper.Playlist().ShuffleEnabled(false); - } - - auto insertIndex = _projectMWrapper.Playlist().CurrentIndex() + 1; - - do - { - Poco::File droppedFile(droppedFilePath); - if (!droppedFile.isDirectory()) - { - // handle dropped preset file - Poco::Path droppedFileP(droppedFilePath); - if (!droppedFile.exists() || (droppedFileP.getExtension() != "milk" && droppedFileP.getExtension() != "prjm")) - { - std::string toastMessage = std::string("Invalid preset file: ") + droppedFilePath; - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); - poco_information_f1(_logger, "%s", toastMessage); - break; // exit the block and go to the shuffle check - } - - if (_projectMWrapper.Playlist().InsertItem(PresetPlaylist::Item(droppedFileP.toString()), insertIndex, true)) - { - if (skipToDropped) - { - _projectMWrapper.Playlist().Next(true); - } - poco_information_f1(_logger, "Added preset: %s", std::string(droppedFilePath)); - // no need to toast single presets, as its obvious if a preset was loaded. - } - } - else - { - // handle dropped directory - _projectMWrapper.Playlist().BeginBatchEdit(); - - // if droppedFolderOverride is enabled, we clear the playlist first - // current edge case: if the dropped directory is invalid or contains no presets, then it still clears the playlist - if (droppedFolderOverride) - { - _projectMWrapper.Playlist().Clear(); - insertIndex = 0; - } - - uint32_t addedFilesCount = _projectMWrapper.Playlist().InsertPath(droppedFilePath, insertIndex, true, true); - if (addedFilesCount > 0) - { - std::string toastMessage = "Added " + std::to_string(addedFilesCount) + " presets from " + droppedFilePath; - poco_information_f1(_logger, "%s", toastMessage); - if (skipToDropped || droppedFolderOverride) - { - // if skip to dropped is true, or if a folder was dropped and it overrode the playlist, we skip to the next preset - _projectMWrapper.Playlist().Next(true); - } - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); - } - else - { - std::string toastMessage = std::string("No presets found in: ") + droppedFilePath; - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); - poco_information_f1(_logger, "%s", toastMessage); - } - - _projectMWrapper.Playlist().EndBatchEdit(); - } - } while (false); - - if (shuffle && skipToDropped) - { - _projectMWrapper.Playlist().ShuffleEnabled(true); - } - - SDL_free(droppedFilePath); + HandleDropEvent(event); break; } @@ -264,15 +180,21 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) switch (keyCode) { case SDLK_ESCAPE: - _projectMGui.Toggle(); - _sdlRenderingWindow.ShowCursor(_projectMGui.Visible()); + if (!modifierPressed) + { + _projectMGui.Toggle(); + _sdlRenderingWindow.ShowCursor(_projectMGui.Visible()); + } break; case SDLK_a: { - bool aspectCorrectionEnabled = !projectm_get_aspect_correction(_projectMHandle); - projectm_set_aspect_correction(_projectMHandle, aspectCorrectionEnabled); + if (!modifierPressed) + { + bool aspectCorrectionEnabled = !projectm_get_aspect_correction(_projectMHandle); + projectm_set_aspect_correction(_projectMHandle, aspectCorrectionEnabled); + } + break; } - break; case SDLK_c: if (modifierPressed) @@ -283,8 +205,11 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) #ifdef _DEBUG case SDLK_d: - // Write next rendered frame to file - projectm_write_debug_image_on_next_frame(_projectMHandle, nullptr); + if (!modifierPressed) + { + // Write next rendered frame to file + projectm_write_debug_image_on_next_frame(_projectMHandle, nullptr); + } break; #endif @@ -306,21 +231,29 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) if (modifierPressed) { _sdlRenderingWindow.NextDisplay(); - break; } _projectMGui.ShowPresetChooser(); break; case SDLK_n: - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::NextPreset, _keyStates._shiftPressed)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::NextPreset, _keyStates._shiftPressed)); + } break; case SDLK_p: - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::PreviousPreset, _keyStates._shiftPressed)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::PreviousPreset, _keyStates._shiftPressed)); + } break; case SDLK_r: { - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::RandomPreset, _keyStates._shiftPressed)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::RandomPreset, _keyStates._shiftPressed)); + } break; } @@ -332,30 +265,45 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) break; case SDLK_y: - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::ToggleShuffle)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::ToggleShuffle)); + } break; case SDLK_BACKSPACE: - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::LastPreset, _keyStates._shiftPressed)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::LastPreset, _keyStates._shiftPressed)); + } break; case SDLK_SPACE: - Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::TogglePresetLocked)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::TogglePresetLocked)); + } break; case SDLK_UP: - // Increase beat sensitivity - _projectMWrapper.ChangeBeatSensitivity(0.01f); + if (!modifierPressed) + { + // Increase beat sensitivity + _projectMWrapper.ChangeBeatSensitivity(0.01f); + } break; case SDLK_DOWN: - // Decrease beat sensitivity - _projectMWrapper.ChangeBeatSensitivity(-0.01f); + if (!modifierPressed) + { + // Decrease beat sensitivity + _projectMWrapper.ChangeBeatSensitivity(-0.01f); + } break; } } -void RenderLoop::ScrollEvent(const SDL_MouseWheelEvent& event) +void RenderLoop::ScrollEvent(const SDL_MouseWheelEvent& event) const { // Wheel up is positive if (event.y > 0) @@ -414,6 +362,8 @@ void RenderLoop::MouseDownEvent(const SDL_MouseButtonEvent& event) projectm_touch_destroy_all(_projectMHandle); poco_debug(_logger, "Cleared all custom waveforms."); break; + + default:; } } @@ -429,3 +379,105 @@ void RenderLoop::QuitNotificationHandler(const Poco::AutoPtr { _wantsToQuit = true; } + +void RenderLoop::HandleDropEvent(const SDL_Event& event) +{ + if (event.drop.file == nullptr) + { + return; + } + + std::string droppedFilePath = event.drop.file; + SDL_free(event.drop.file); + + Poco::File droppedFile(droppedFilePath); + if (!droppedFile.exists() && !droppedFile.isFile() && !droppedFile.isDirectory()) + { + const std::string toastMessage = std::string("Invalid file or directory dropped: ") + droppedFilePath; + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); + poco_information(_logger, toastMessage); + return; + } + + // first we want to get the config settings that are relevant ehre + // namely skipToDropped and droppedFolderOverride + // we can get them from the projectMWrapper, in the _projectMConfigView available on it + const bool skipToDropped = _userConfig->getBool("projectM.skipToDropped", true); + const bool droppedFolderOverride = _userConfig->getBool("projectM.droppedFolderOverride", false); + + const bool shuffle = _projectMWrapper.Playlist().ShuffleEnabled(); + if (shuffle && skipToDropped) + { + // if shuffle is enabled, we disable it temporarily, so the dropped preset is played next + // if skipToDropped is false, we also keep shuffle enabled, as it doesn't matter since the current preset is unaffected + _projectMWrapper.Playlist().ShuffleEnabled(false); + } + + auto insertIndex = _projectMWrapper.Playlist().CurrentIndex() + 1; + + do + { + Poco::File droppedFile(droppedFilePath); + if (!droppedFile.isDirectory()) + { + // handle dropped preset file + const Poco::Path droppedFileP(droppedFilePath); + if (droppedFileP.getExtension() != "milk" && droppedFileP.getExtension() != "prjm") + { + const std::string toastMessage = std::string("Invalid file extension: ") + droppedFilePath; + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); + poco_information(_logger, toastMessage); + break; // exit the block and go to the shuffle check + } + + if (_projectMWrapper.Playlist().InsertItem(PresetPlaylist::Item(droppedFileP.toString()), insertIndex, true)) + { + if (skipToDropped) + { + _projectMWrapper.Playlist().Next(true); + } + poco_information_f1(_logger, "Added preset: %s", std::string(droppedFilePath)); + // no need to toast single presets, as its obvious if a preset was loaded. + } + } + else + { + // handle dropped directory + _projectMWrapper.Playlist().BeginBatchEdit(); + + // if droppedFolderOverride is enabled, we clear the playlist first + // current edge case: if the dropped directory is invalid or contains no presets, then it still clears the playlist + if (droppedFolderOverride) + { + _projectMWrapper.Playlist().Clear(); + insertIndex = 0; + } + + uint32_t addedFilesCount = _projectMWrapper.Playlist().InsertPath(droppedFilePath, insertIndex, true, true); + if (addedFilesCount > 0) + { + std::string toastMessage = "Added " + std::to_string(addedFilesCount) + " presets from " + droppedFilePath; + poco_information_f1(_logger, "%s", toastMessage); + if (skipToDropped || droppedFolderOverride) + { + // if skip to dropped is true, or if a folder was dropped and it overrode the playlist, we skip to the next preset + _projectMWrapper.Playlist().Next(true); + } + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); + } + else + { + const std::string toastMessage = std::string("No presets found in directory: ") + droppedFilePath; + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::DisplayToast(toastMessage)); + poco_information_f1(_logger, "%s", toastMessage); + } + + _projectMWrapper.Playlist().EndBatchEdit(); + } + } while (false); + + if (shuffle && skipToDropped) + { + _projectMWrapper.Playlist().ShuffleEnabled(true); + } +} \ No newline at end of file diff --git a/src/RenderLoop.h b/src/RenderLoop.h index 12df785a..a0ff9a4a 100644 --- a/src/RenderLoop.h +++ b/src/RenderLoop.h @@ -8,7 +8,6 @@ #include #include -#include class ProjectMGUI; @@ -52,7 +51,7 @@ class RenderLoop * @brief Handles SDL mouse wheel events. * @param event The mouse wheel event */ - void ScrollEvent(const SDL_MouseWheelEvent& event); + void ScrollEvent(const SDL_MouseWheelEvent& event) const; /** * @brief Handles SDL mouse button down events. @@ -72,6 +71,12 @@ class RenderLoop */ void QuitNotificationHandler(const Poco::AutoPtr& notification); + /** + * Handles a file/dir drop event into the window. + * @param event The drop event from SDL + */ + void HandleDropEvent(const SDL_Event& event); + AudioCapture& _audioCapture; ProjectMWrapper& _projectMWrapper; SDLRenderingWindow& _sdlRenderingWindow;