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/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/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 f176a348..328733cd 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 @@ -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,24 +191,26 @@ 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( - 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() @@ -220,62 +229,53 @@ 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); + if (_playlist && !_playlist->Empty()) + { + SDL_SetClipboardText(_playlist->CurrentItem().Path().c_str()); + } } -void ProjectMWrapper::PresetSwitchedEvent(bool isHardCut, unsigned int index, void* context) +void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) { - 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 UpdateWindowTitleNotification); -} + if (!_playlist) + { + return; + } -void ProjectMWrapper::PlaybackControlNotificationHandler(const Poco::AutoPtr& notification) -{ - bool shuffleEnabled = projectm_playlist_get_shuffle(_playlist); + bool shuffleEnabled = _playlist->ShuffleEnabled(); + bool hardCut = !notification->SmoothTransition(); switch (notification->ControlAction()) { - case PlaybackControlNotification::Action::NextPreset: - projectm_playlist_set_shuffle(_playlist, false); - projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + case Notification::PlaybackControl::Action::NextPreset: + _playlist->Next(hardCut); break; - case PlaybackControlNotification::Action::PreviousPreset: - projectm_playlist_set_shuffle(_playlist, false); - projectm_playlist_play_previous(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + case Notification::PlaybackControl::Action::PreviousPreset: + _playlist->Previous(hardCut); break; - case PlaybackControlNotification::Action::LastPreset: - projectm_playlist_play_last(_playlist, !notification->SmoothTransition()); + case Notification::PlaybackControl::Action::LastPreset: + _playlist->LastInHistory(hardCut); break; - case PlaybackControlNotification::Action::RandomPreset: { - projectm_playlist_set_shuffle(_playlist, true); - projectm_playlist_play_next(_playlist, !notification->SmoothTransition()); - projectm_playlist_set_shuffle(_playlist, shuffleEnabled); + case Notification::PlaybackControl::Action::RandomPreset: { + _playlist->Random(hardCut); 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,12 +327,12 @@ 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") { - 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 c8eb89d8..8af28286 100644 --- a/src/ProjectMWrapper.h +++ b/src/ProjectMWrapper.h @@ -1,10 +1,10 @@ #pragma once -#include "notifications/AudioDataAvailableNotification.h" -#include "notifications/PlaybackControlNotification.h" +#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,17 +88,9 @@ 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 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); @@ -112,14 +110,14 @@ 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. 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..9dc58d81 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() @@ -107,89 +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 = projectm_playlist_get_shuffle(_playlistHandle); - 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); - } - - int index = projectm_playlist_get_position(_playlistHandle) + 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 DisplayToastNotification(toastMessage)); - poco_information_f1(_logger, "%s", toastMessage); - break; // exit the block and go to the shuffle check - } - - if (projectm_playlist_insert_preset(_playlistHandle, droppedFilePath, index, true)) - { - if (skipToDropped) - { - projectm_playlist_play_next(_playlistHandle, 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 - - // 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; - } - - uint32_t addedFilesCount = projectm_playlist_insert_path(_playlistHandle, droppedFilePath, index, 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 - projectm_playlist_play_next(_playlistHandle, true); - } - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(toastMessage)); - } - else - { - std::string toastMessage = std::string("No presets found in: ") + droppedFilePath; - Poco::NotificationCenter::defaultCenter().postNotification(new DisplayToastNotification(toastMessage)); - poco_information_f1(_logger, "%s", toastMessage); - } - } - } while (false); - - if (shuffle && skipToDropped) - { - projectm_playlist_set_shuffle(_playlistHandle, true); - } - - SDL_free(droppedFilePath); + HandleDropEvent(event); break; } @@ -265,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) @@ -284,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 @@ -307,20 +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 PlaybackControlNotification(PlaybackControlNotification::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 PlaybackControlNotification(PlaybackControlNotification::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 PlaybackControlNotification(PlaybackControlNotification::Action::RandomPreset, _keyStates._shiftPressed)); + if (!modifierPressed) + { + Poco::NotificationCenter::defaultCenter().postNotification(new Notification::PlaybackControl(Notification::PlaybackControl::Action::RandomPreset, _keyStates._shiftPressed)); + } break; } @@ -332,40 +265,55 @@ void RenderLoop::KeyEvent(const SDL_KeyboardEvent& event, bool down) break; case SDLK_y: - Poco::NotificationCenter::defaultCenter().postNotification(new PlaybackControlNotification(PlaybackControlNotification::Action::ToggleShuffle)); + if (!modifierPressed) + { + 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)); + if (!modifierPressed) + { + 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)); + 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) { - 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); } } @@ -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:; } } @@ -425,7 +375,109 @@ void RenderLoop::MouseUpEvent(const SDL_MouseButtonEvent& event) } } -void RenderLoop::QuitNotificationHandler(const Poco::AutoPtr& notification) +void RenderLoop::QuitNotificationHandler(const Poco::AutoPtr& notification) { _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 9429b1ff..a0ff9a4a 100644 --- a/src/RenderLoop.h +++ b/src/RenderLoop.h @@ -4,11 +4,10 @@ #include "ProjectMWrapper.h" #include "SDLRenderingWindow.h" -#include "notifications/QuitNotification.h" +#include "notifications/Quit.h" #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. @@ -70,18 +69,23 @@ class RenderLoop * @brief Handler for quit notifications. * @param notification The received notification. */ - void QuitNotificationHandler(const Poco::AutoPtr& notification); + 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; projectm_handle _projectMHandle{nullptr}; - projectm_playlist_handle _playlistHandle{nullptr}; 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..5caa0729 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(); } @@ -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/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/CMakeLists.txt b/src/gui/CMakeLists.txt index a2c72ee3..a9989b97 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -29,8 +29,8 @@ add_library(ProjectMSDL-GUI STATIC HelpWindow.h MainMenu.cpp MainMenu.h - PresetSelection.cpp - PresetSelection.h + PresetChooser.cpp + PresetChooser.h ProjectMGUI.cpp ProjectMGUI.h SettingsWindow.cpp 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. 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/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/PresetSelection.cpp b/src/gui/PresetSelection.cpp deleted file mode 100644 index 7ecb6c57..00000000 --- a/src/gui/PresetSelection.cpp +++ /dev/null @@ -1,70 +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(); - projectm_playlist_set_position(projectMWrapper.Playlist(), _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(); - 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); - } - - 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. - -}; diff --git a/src/gui/ProjectMGUI.cpp b/src/gui/ProjectMGUI.cpp index 925c8438..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; @@ -234,7 +262,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..ae815997 100644 --- a/src/gui/ProjectMGUI.h +++ b/src/gui/ProjectMGUI.h @@ -3,10 +3,11 @@ #include "AboutWindow.h" #include "HelpWindow.h" #include "MainMenu.h" +#include "PresetChooser.h" #include "SettingsWindow.h" #include "ToastMessage.h" -#include "notifications/DisplayToastNotification.h" +#include "notifications/DisplayToast.h" #include @@ -104,16 +105,26 @@ 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(); 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.) @@ -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. 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..2493baf3 100644 --- a/src/notifications/CMakeLists.txt +++ b/src/notifications/CMakeLists.txt @@ -1,14 +1,16 @@ 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 + PlaylistChanged.cpp + PlaylistChanged.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/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 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"; -}