diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cbcd76314..9c188a0c95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,7 @@ jobs: libchromaprint-devel fftw3-devel libebur128-devel + projectM-devel desktop-file-utils update-desktop-files appstream-glib @@ -85,6 +86,7 @@ jobs: libQt5Network-devel libQt5Sql-devel libQt5DBus-devel + libQt5OpenGL-devel libQt5Test-devel libqt5-qtbase-common-devel libQt5Sql5-sqlite @@ -105,6 +107,7 @@ jobs: qt6-base-common-devel qt6-sql-sqlite qt6-linguist-devel + qt6-opengl-devel - name: Install tagparser if: matrix.opensuse_version == 'tumbleweed' run: zypper -n --gpg-auto-import-keys in tagparser-devel @@ -219,6 +222,7 @@ jobs: libchromaprint-devel libebur128-devel fftw-devel + libprojectM-devel desktop-file-utils libappstream-glib hicolor-icon-theme @@ -310,6 +314,7 @@ jobs: lib64Qt6DBus-devel lib64Qt6Gui-devel lib64Qt6Widgets-devel + lib64Qt6OpenGL-devel lib64Qt6Test-devel qt6-cmake qt6-qtbase-tools @@ -406,6 +411,7 @@ jobs: lib64fftw-devel lib64dbus-devel lib64appstream-devel + lib64projectm-devel lib64qt6core-devel lib64qt6gui-devel lib64qt6widgets-devel @@ -415,6 +421,7 @@ jobs: lib64qt6dbus-devel lib64qt6help-devel lib64qt6test-devel + lib64qt6opengl-devel protobuf-compiler desktop-file-utils appstream-util @@ -505,11 +512,12 @@ jobs: libcdio-dev libmtp-dev libgpod-dev + libprojectm-dev - name: Install Qt 5 if: matrix.debian_version == 'bullseye' env: DEBIAN_FRONTEND: noninteractive - run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev + run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev libqt5opengl5-dev - name: Install Qt 6 if: matrix.debian_version != 'bullseye' env: @@ -592,11 +600,12 @@ jobs: libcdio-dev libmtp-dev libgpod-dev + libprojectm-dev - name: Install Qt 5 if: matrix.ubuntu_version == 'focal' env: DEBIAN_FRONTEND: noninteractive - run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev + run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev libqt5opengl5-dev - name: Install Qt 6 if: matrix.ubuntu_version != 'focal' env: @@ -678,6 +687,7 @@ jobs: libcdio-dev libmtp-dev libgpod-dev + libprojectm-dev gstreamer1.0-alsa gstreamer1.0-pulseaudio protobuf-compiler @@ -690,7 +700,7 @@ jobs: if: matrix.ubuntu_version == 'focal' env: DEBIAN_FRONTEND: noninteractive - run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev + run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev libqt5opengl5-dev - name: Install Qt 6 if: matrix.ubuntu_version != 'focal' env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d3ce6d409..6dadc6fb03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,14 @@ pkg_check_modules(LIBMTP libmtp>=1.0) pkg_check_modules(GDK_PIXBUF gdk-pixbuf-2.0) find_package(Gettext) find_package(FFTW3) +find_package(projectM4 COMPONENTS Playlist) +if(projectM4_FOUND) + set(LIBPROJECTM_FOUND ON) + set(HAVE_PROJECTM4 ON) + set(LIBPROJECTM_LIBRARIES libprojectM::projectM libprojectM::playlist) +else() + pkg_check_modules(LIBPROJECTM libprojectM) +endif() find_package(GTest) find_library(GMOCK_LIBRARY gmock) @@ -185,6 +193,11 @@ endif() if(X11_FOUND AND QT_VERSION_MAJOR EQUAL 5) list(APPEND QT_COMPONENTS X11Extras) endif() +if(QT_VERSION_MAJOR EQUAL 5) + list(APPEND QT_OPTIONAL_COMPONENTS OpenGL) +else() + list(APPEND QT_OPTIONAL_COMPONENTS OpenGLWidgets) +endif() find_package(Qt${QT_VERSION_MAJOR} ${QT_MIN_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED OPTIONAL_COMPONENTS ${QT_OPTIONAL_COMPONENTS}) @@ -449,6 +462,20 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization" DEPENDS "gstreamer" HAVE_GSTREAMER ) +if(QT_VERSION_MAJOR EQUAL 5) + optional_component(VISUALIZATIONS ON "Visualizations" + DEPENDS "libprojectm" LIBPROJECTM_FOUND + DEPENDS "QtOpenGL" Qt${QT_VERSION_MAJOR}OpenGL_FOUND + DEPENDS "gstreamer" HAVE_GSTREAMER + ) +else() + optional_component(VISUALIZATIONS ON "Visualizations" + DEPENDS "libprojectm" LIBPROJECTM_FOUND + DEPENDS "QtOpenGLWidgets" Qt${QT_VERSION_MAJOR}OpenGLWidgets_FOUND + DEPENDS "gstreamer" HAVE_GSTREAMER + ) +endif() + if(APPLE OR WIN32) set(USE_BUNDLE_DEFAULT ON) else() diff --git a/debian/CMakeLists.txt b/debian/CMakeLists.txt index 9cbe257511..f12a19f8c5 100644 --- a/debian/CMakeLists.txt +++ b/debian/CMakeLists.txt @@ -6,7 +6,7 @@ if(LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE) if(DEB_CODENAME AND DEB_DATE) if(QT_VERSION_MAJOR EQUAL 5) - set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qtbase5-dev,qtbase5-dev-tools,qttools5-dev,qttools5-dev-tools,libqt5x11extras5-dev) + set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qtbase5-dev,qtbase5-dev-tools,qttools5-dev,qttools5-dev-tools,libqt5x11extras5-dev,libqt5opengl5-dev) set(DEBIAN_DEPENDS_QT_PACKAGES libqt5sql5-sqlite) endif() if(QT_VERSION_MAJOR EQUAL 6) diff --git a/debian/control.in b/debian/control.in index c8d838f3df..8ebc985b5c 100644 --- a/debian/control.in +++ b/debian/control.in @@ -26,7 +26,8 @@ Build-Depends: debhelper (>= 11), libmtp-dev, libchromaprint-dev, libfftw3-dev, - libebur128-dev + libebur128-dev, + libprojectm-dev Standards-Version: 4.6.1 Package: strawberry diff --git a/dist/unix/strawberry.spec.in b/dist/unix/strawberry.spec.in index 9394134b1e..362b3002e2 100644 --- a/dist/unix/strawberry.spec.in +++ b/dist/unix/strawberry.spec.in @@ -46,6 +46,7 @@ BuildRequires: pkgconfig(taglib) BuildRequires: pkgconfig(fftw3) BuildRequires: pkgconfig(icu-uc) BuildRequires: pkgconfig(icu-i18n) +BuildRequires: pkgconfig(libprojectM) BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Core) BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Concurrent) BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Network) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 65e82a1fc3..37c0f78beb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -975,6 +975,27 @@ optional_source(HAVE_EBUR128 SOURCES engine/ebur128analysis.cpp ) +# Visualizations +optional_source(HAVE_VISUALIZATIONS + SOURCES + visualizations/projectmpresetmodel.cpp + visualizations/projectmvisualization.cpp + visualizations/visualizationcontainer.cpp + visualizations/visualizationoverlay.cpp + visualizations/visualizationselector.cpp + visualizations/visualizationopenglwidget.cpp + HEADERS + visualizations/projectmpresetmodel.h + visualizations/projectmvisualization.h + visualizations/visualizationcontainer.h + visualizations/visualizationoverlay.h + visualizations/visualizationselector.h + visualizations/visualizationopenglwidget.h + UI + visualizations/visualizationoverlay.ui + visualizations/visualizationselector.ui +) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) @@ -1089,6 +1110,10 @@ if(HAVE_QTSPARKLE) link_directories(${QTSPARKLE_LIBRARY_DIRS}) endif() +if(HAVE_VISUALIZATIONS) + link_directories(${LIBPROJECTM_LIBRARY_DIRS}) +endif() + add_library(strawberry_lib STATIC ${SOURCES} ${MOC} @@ -1149,6 +1174,14 @@ if(HAVE_X11_GLOBALSHORTCUTS AND HAVE_X11EXTRAS) target_link_libraries(strawberry_lib PUBLIC Qt${QT_VERSION_MAJOR}::X11Extras) endif() +if(HAVE_VISUALIZATIONS) + if(QT_VERSION_MAJOR EQUAL 5) + target_link_libraries(strawberry_lib PUBLIC Qt${QT_VERSION_MAJOR}::OpenGL) + else() + target_link_libraries(strawberry_lib PUBLIC Qt${QT_VERSION_MAJOR}::OpenGLWidgets) + endif() +endif() + if(HAVE_ALSA) target_include_directories(strawberry_lib SYSTEM PRIVATE ${ALSA_INCLUDE_DIRS}) target_link_libraries(strawberry_lib PRIVATE ${ALSA_LIBRARIES}) @@ -1231,6 +1264,11 @@ if(HAVE_LIBMTP) target_link_libraries(strawberry_lib PRIVATE ${LIBMTP_LIBRARIES}) endif() +if(HAVE_VISUALIZATIONS) + target_include_directories(strawberry_lib SYSTEM PRIVATE ${LIBPROJECTM_INCLUDE_DIRS}) + target_link_libraries(strawberry_lib PRIVATE ${LIBPROJECTM_LIBRARIES}) +endif() + if(APPLE) target_link_libraries(strawberry_lib PRIVATE "-framework AppKit" diff --git a/src/analyzer/analyzercontainer.cpp b/src/analyzer/analyzercontainer.cpp index 4fbcbe7952..4a78be1736 100644 --- a/src/analyzer/analyzercontainer.cpp +++ b/src/analyzer/analyzercontainer.cpp @@ -72,7 +72,8 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent) double_click_timer_(new QTimer(this)), ignore_next_click_(false), current_analyzer_(nullptr), - engine_(nullptr) { + engine_(nullptr), + action_visualization_(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); setLayout(layout); @@ -125,6 +126,17 @@ void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) { } +void AnalyzerContainer::mouseDoubleClickEvent(QMouseEvent *e) { + + Q_UNUSED(e); + + double_click_timer_->stop(); + ignore_next_click_ = true; + + if (action_visualization_) action_visualization_->trigger(); + +} + void AnalyzerContainer::ShowPopupMenu() { context_menu_->popup(last_click_pos_); } @@ -247,3 +259,10 @@ void AnalyzerContainer::AddFramerate(const QString &name, const int framerate) { QObject::connect(action, &QAction::triggered, this, [this, framerate]() { ChangeFramerate(framerate); } ); } + +void AnalyzerContainer::SetVisualizationsAction(QAction *visualization) { + + action_visualization_ = visualization; + context_menu_->addAction(action_visualization_); + +} diff --git a/src/analyzer/analyzercontainer.h b/src/analyzer/analyzercontainer.h index 514bfa8796..9bd1e698b5 100644 --- a/src/analyzer/analyzercontainer.h +++ b/src/analyzer/analyzercontainer.h @@ -46,6 +46,7 @@ class AnalyzerContainer : public QWidget { explicit AnalyzerContainer(QWidget *parent); void SetEngine(SharedPtr engine); + void SetVisualizationsAction(QAction *visualization); static const char *kSettingsGroup; static const char *kSettingsFramerate; @@ -55,6 +56,7 @@ class AnalyzerContainer : public QWidget { protected: void mouseReleaseEvent(QMouseEvent *e) override; + void mouseDoubleClickEvent(QMouseEvent *e) override; void wheelEvent(QWheelEvent *e) override; private slots: @@ -89,6 +91,8 @@ class AnalyzerContainer : public QWidget { AnalyzerBase *current_analyzer_; SharedPtr engine_; + + QAction *action_visualization_; }; template diff --git a/src/config.h.in b/src/config.h.in index e309faea9a..d30198dae4 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -57,4 +57,7 @@ #cmakedefine HAVE_EBUR128 +#cmakedefine HAVE_VISUALIZATIONS +#cmakedefine HAVE_PROJECTM4 + #endif // CONFIG_H_IN diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 5731247d3f..8c970e2aae 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -211,6 +211,11 @@ #include "organize/organizeerrordialog.h" +#ifdef HAVE_VISUALIZATIONS +# include "visualizations/visualizationcontainer.h" +# include "engine/gstengine.h" +#endif + #ifdef Q_OS_WIN # include "windows7thumbbar.h" #endif @@ -594,6 +599,12 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS stop_menu->addAction(ui_->action_stop_after_this_track); ui_->stop_button->setMenu(stop_menu); +#ifdef HAVE_VISUALIZATIONS + QObject::connect(ui_->action_visualizations, &QAction::triggered, this, &MainWindow::ShowVisualizations); +#else + ui_->action_visualizations->setEnabled(false); +#endif + // Player connections QObject::connect(ui_->volume, &VolumeSlider::valueChanged, &*app_->player(), &Player::SetVolumeFromSlider); @@ -859,6 +870,7 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS // Analyzer QObject::connect(ui_->analyzer, &AnalyzerContainer::WheelEvent, this, &MainWindow::VolumeWheelEvent); + ui_->analyzer->SetVisualizationsAction(ui_->action_visualizations); // Statusbar widgets ui_->playlist_summary->setMinimumWidth(QFontMetrics(font()).horizontalAdvance(QStringLiteral("WW selected of WW tracks - [ WW:WW ]"))); @@ -3330,3 +3342,24 @@ void MainWindow::FocusSearchField() { } } + +void MainWindow::ShowVisualizations() { + +#ifdef HAVE_VISUALIZATIONS + + if (!visualization_) { + visualization_.reset(new VisualizationContainer); + + visualization_->SetActions(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_next_track); + connect(&*app_->player(), &Player::Stopped, &*visualization_, &VisualizationContainer::Stopped); + connect(&*app_->player(), &Player::ForceShowOSD, &*visualization_, &VisualizationContainer::SongMetadataChanged); + connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*visualization_, &VisualizationContainer::SongMetadataChanged); + + visualization_->SetEngine(qobject_cast(&*app_->player()->engine())); + } + + visualization_->show(); + +#endif // HAVE_VISUALIZATIONS + +} diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index d2f9943b25..11d58ce016 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -101,6 +101,7 @@ class Windows7ThumbBar; class AddStreamDialog; class LastFMImportDialog; class RadioViewContainer; +class VisualizationContainer; class MainWindow : public QMainWindow, public PlatformInterface { Q_OBJECT @@ -274,6 +275,8 @@ class MainWindow : public QMainWindow, public PlatformInterface { void DeleteFilesFinished(const SongList &songs_with_errors); + void ShowVisualizations(); + public slots: void CommandlineOptionsReceived(const QByteArray &string_options); void Raise(); @@ -358,6 +361,10 @@ class MainWindow : public QMainWindow, public PlatformInterface { LastFMImportDialog *lastfm_import_dialog_; +#ifdef HAVE_VISUALIZATIONS + ScopedPtr visualization_; +#endif + QAction *collection_show_all_; QAction *collection_show_duplicates_; QAction *collection_show_untagged_; diff --git a/src/core/mainwindow.ui b/src/core/mainwindow.ui index 826fdbe2de..56477f22eb 100644 --- a/src/core/mainwindow.ui +++ b/src/core/mainwindow.ui @@ -521,6 +521,7 @@ + @@ -864,6 +865,11 @@ Import data from last.fm... + + + Visualizations + + diff --git a/src/visualizations/projectmpresetmodel.cpp b/src/visualizations/projectmpresetmodel.cpp new file mode 100644 index 0000000000..4dff16fd8a --- /dev/null +++ b/src/visualizations/projectmpresetmodel.cpp @@ -0,0 +1,159 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" + +#include "projectmpresetmodel.h" +#include "projectmvisualization.h" + +ProjectMPresetModel::ProjectMPresetModel(ProjectMVisualization *projectm_visualization, QObject *parent) + : QAbstractItemModel(parent), + projectm_visualization_(projectm_visualization) { + + // Find presets + if (QFileInfo::exists(projectm_visualization_->preset_path())) { + QDirIterator it(projectm_visualization_->preset_path(), QStringList() << QStringLiteral("*.milk") << QStringLiteral("*.prjm"), QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, QDirIterator::Subdirectories); + QStringList files; + while (it.hasNext()) { + it.next(); + files << it.filePath(); + } + std::stable_sort(files.begin(), files.end()); + for (const QString &filepath : std::as_const(files)) { + const QFileInfo fileinfo(filepath); + all_presets_ << Preset(fileinfo.filePath(), fileinfo.fileName(), false); + } + } + else { + qLog(Error) << "ProjectM preset path" << projectm_visualization_->preset_path() << "does not exist"; + } + +} + +int ProjectMPresetModel::rowCount(const QModelIndex &idx) const { + + Q_UNUSED(idx); + + if (!projectm_visualization_) return 0; + + return static_cast(all_presets_.count()); + +} + +int ProjectMPresetModel::columnCount(const QModelIndex &idx) const { + Q_UNUSED(idx); + return 1; +} + +QModelIndex ProjectMPresetModel::index(const int row, const int column, const QModelIndex &idx) const { + Q_UNUSED(idx); + return createIndex(row, column); +} + +QModelIndex ProjectMPresetModel::parent(const QModelIndex &child) const { + Q_UNUSED(child); + return QModelIndex(); +} + +QVariant ProjectMPresetModel::data(const QModelIndex &index, const int role) const { + + switch (role) { + case Qt::DisplayRole: + return all_presets_[index.row()].name_; + case Qt::CheckStateRole:{ + bool selected = all_presets_[index.row()].selected_; + return selected ? Qt::Checked : Qt::Unchecked; + } + case Role::Role_Path: + return all_presets_[index.row()].path_; + default: + return QVariant(); + } + +} + +Qt::ItemFlags ProjectMPresetModel::flags(const QModelIndex &idx) const { + + if (!idx.isValid()) return QAbstractItemModel::flags(idx); + return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled; + +} + +bool ProjectMPresetModel::setData(const QModelIndex &idx, const QVariant &value, int role) { + + if (role == Qt::CheckStateRole) { + all_presets_[idx.row()].selected_ = value.toBool(); + projectm_visualization_->SetSelected(QStringList() << all_presets_[idx.row()].path_, value.toBool()); + return true; + } + + return false; + +} + +void ProjectMPresetModel::SetImmediatePreset(const QModelIndex &idx) { + projectm_visualization_->SetImmediatePreset(all_presets_[idx.row()].path_); +} + +void ProjectMPresetModel::SelectAll() { + + QStringList paths; + paths.reserve(all_presets_.count()); + for (int i = 0; i < all_presets_.count(); ++i) { + paths << all_presets_[i].path_; + all_presets_[i].selected_ = true; + } + projectm_visualization_->SetSelected(paths, true); + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + +} + +void ProjectMPresetModel::SelectNone() { + + projectm_visualization_->ClearSelected(); + for (int i = 0; i < all_presets_.count(); ++i) { + all_presets_[i].selected_ = false; + } + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); + +} + +void ProjectMPresetModel::MarkSelected(const QString &path, const bool selected) { + + for (int i = 0; i < all_presets_.count(); ++i) { + if (path == all_presets_[i].path_) { + all_presets_[i].selected_ = selected; + return; + } + } + +} diff --git a/src/visualizations/projectmpresetmodel.h b/src/visualizations/projectmpresetmodel.h new file mode 100644 index 0000000000..a56b754cd2 --- /dev/null +++ b/src/visualizations/projectmpresetmodel.h @@ -0,0 +1,77 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PROJECTMPRESETMODEL_H +#define PROJECTMPRESETMODEL_H + +#include "config.h" + +#include +#include +#include + +class ProjectMVisualization; + +class ProjectMPresetModel : public QAbstractItemModel { + Q_OBJECT + + friend class ProjectMVisualization; + + public: + explicit ProjectMPresetModel(ProjectMVisualization *projectm_visualization, QObject *parent = nullptr); + + enum Role { + Role_Path = Qt::UserRole, + }; + + void MarkSelected(const QString &path, bool selected); + + // QAbstractItemModel + QModelIndex index(const int row, const int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, const int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, const int role = Qt::EditRole) override; + + public slots: + void SetImmediatePreset(const QModelIndex &index); + void SelectAll(); + void SelectNone(); + + private: + struct Preset { + explicit Preset(const QString &path, const QString &name, const bool selected) + : path_(path), + name_(name), + selected_(selected) {} + + QString path_; + QString name_; + bool selected_; + }; + + ProjectMVisualization *projectm_visualization_; + QList all_presets_; +}; + +#endif // PROJECTMPRESETMODEL_H diff --git a/src/visualizations/projectmvisualization.cpp b/src/visualizations/projectmvisualization.cpp new file mode 100644 index 0000000000..d64ac562cd --- /dev/null +++ b/src/visualizations/projectmvisualization.cpp @@ -0,0 +1,469 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include + +#ifdef HAVE_PROJECTM4 +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#else +# include +#endif // HAVE_PROJECTM4 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/settings.h" +#include "projectmvisualization.h" +#include "projectmpresetmodel.h" +#include "visualizationcontainer.h" + +ProjectMVisualization::ProjectMVisualization(VisualizationContainer *container) + : QGraphicsScene(container), + container_(container), + preset_model_(nullptr), +#ifdef HAVE_PROJECTM4 + projectm_instance_(nullptr), + projectm_playlist_instance_(nullptr), +#endif + mode_(Mode::Random), + duration_(15), + texture_size_(512) { + + QObject::connect(this, &QGraphicsScene::sceneRectChanged, this, &ProjectMVisualization::SceneRectChanged); + +#ifndef HAVE_PROJECTM4 + for (int i = 0; i < TOTAL_RATING_TYPES; ++i) { + default_rating_list_.push_back(3); + } +#endif // HAVE_PROJECTM4 + +} + +ProjectMVisualization::~ProjectMVisualization() { + +#ifdef HAVE_PROJECTM4 + if (projectm_playlist_instance_) { + projectm_playlist_destroy(projectm_playlist_instance_); + } + if (projectm_instance_) { + projectm_destroy(projectm_instance_); + } +#endif // HAVE_PROJECTM4 + +} + +void ProjectMVisualization::Init() { + +#ifdef HAVE_PROJECTM4 + if (projectm_instance_) { + return; + } +#else + if (projectm_) { + return; + } +#endif // HAVE_PROJECTM4 + + // Find the projectM presets + + QStringList data_paths = QStringList() << QStringLiteral("/usr/share") + << QStringLiteral("/usr/local/share") + << QLatin1String(CMAKE_INSTALL_PREFIX) + QLatin1String("/share"); + + const QStringList xdg_data_dirs = QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(QLatin1Char(':')); + for (const QString &xdg_data_dir : xdg_data_dirs) { + if (!data_paths.contains(xdg_data_dir)) { + data_paths.append(xdg_data_dir); + } + } + +#if defined(Q_OS_WIN32) + data_paths.prepend(QCoreApplication::applicationDirPath()); +#endif + + const QStringList projectm_paths = QStringList() << QStringLiteral("projectM/presets") + << QStringLiteral("projectm-presets"); + + QStringList preset_paths; + for (const QString &data_path : std::as_const(data_paths)) { + for (const QString &projectm_path : projectm_paths) { + const QString path = data_path + QLatin1Char('/') + projectm_path; + if (!QFileInfo::exists(path) || QDir(path).entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot).isEmpty()) { + preset_paths << path; + continue; + } + preset_path_ = path; + break; + } + } + + // Create projectM settings +#ifdef HAVE_PROJECTM4 + Q_ASSERT(projectm_instance_ == nullptr); + Q_ASSERT(projectm_playlist_instance_ == nullptr); + projectm_instance_ = projectm_create(); + projectm_set_preset_duration(projectm_instance_, duration_); + projectm_set_mesh_size(projectm_instance_, 32, 24); + projectm_set_fps(projectm_instance_, 35); + projectm_set_window_size(projectm_instance_, 512, 512); + projectm_playlist_instance_ = projectm_playlist_create(projectm_instance_); +#else + projectM::Settings s; + s.presetURL = preset_path_.toStdString(); + s.meshX = 32; + s.meshY = 24; + s.textureSize = texture_size_; + s.fps = 35; + s.windowWidth = 512; + s.windowHeight = 512; + s.smoothPresetDuration = 5; + s.presetDuration = duration_; + s.shuffleEnabled = true; + s.softCutRatingsEnabled = false; + s.easterEgg = 0; + projectm_ = std::make_unique(s); +#endif // HAVE_PROJECTM4 + + Q_ASSERT(preset_model_ == nullptr); + preset_model_ = new ProjectMPresetModel(this, this); + + Load(); + + // Start at a random preset. +#ifdef HAVE_PROJECTM4 + const uint count = projectm_playlist_size(projectm_playlist_instance_); + if (count > 0) { + const uint position = QRandomGenerator::global()->bounded(count); + projectm_playlist_set_position(projectm_playlist_instance_, position, true); + } +#else + const uint count = projectm_->getPlaylistSize(); + if (count > 0) { + const uint selection = QRandomGenerator::global()->bounded(count); + projectm_->selectPreset(selection, true); + } +#endif // HAVE_PROJECTM4 + + if (preset_path_.isEmpty()) { + qWarning("ProjectM presets could not be found, search path was:\n %s", preset_paths.join(QLatin1String("\n ")).toLocal8Bit().constData()); + QMessageBox::warning(nullptr, tr("Missing projectM presets"), tr("Strawberry could not load any projectM visualizations. Check that you have installed Strawberry properly.")); + } + + Resize(sceneRect().width(), sceneRect().height(), container_->devicePixelRatio()); + +} + +void ProjectMVisualization::drawBackground(QPainter *p, const QRectF &rect) { + + Q_UNUSED(rect); + + p->beginNativePainting(); + +#ifdef HAVE_PROJECTM4 + projectm_opengl_render_frame(projectm_instance_); +#else + projectm_->renderFrame(); +#endif + + p->endNativePainting(); + +} + +void ProjectMVisualization::SceneRectChanged(const QRectF &rect) { + + Resize(rect.width(), rect.height(), container_->devicePixelRatio()); + +} + +void ProjectMVisualization::Resize(const qreal width, const qreal height, const qreal pixel_ratio) { + +#ifdef HAVE_PROJECTM4 + if (projectm_instance_) { + projectm_set_window_size(projectm_instance_, static_cast(width * pixel_ratio), static_cast(height * pixel_ratio)); + } +#else + if (projectm_) { + projectm_->projectM_resetGL(static_cast(width * pixel_ratio), static_cast(height * pixel_ratio)); + } +#endif // HAVE_PROJECTM4 + +} + +void ProjectMVisualization::SetTextureSize(const int size) { + + texture_size_ = size; + +#ifndef HAVE_PROJECTM4 + if (projectm_) { + projectm_->changeTextureSize(texture_size_); + } +#endif // HAVE_PROJECTM4 + +} + +void ProjectMVisualization::SetDuration(const int seconds) { + + duration_ = seconds; + +#ifdef HAVE_PROJECTM4 + if (projectm_instance_) { + projectm_set_preset_duration(projectm_instance_, duration_); + } +#else + if (projectm_) { + projectm_->changePresetDuration(duration_); + } +#endif // HAVE_PROJECTM4 + + Save(); + +} + +void ProjectMVisualization::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) { + + Q_UNUSED(pipeline_id); + Q_UNUSED(format); + + GstMapInfo map; + gst_buffer_map(buffer, &map, GST_MAP_READ); + +#ifdef HAVE_PROJECTM4 + if (projectm_instance_) { + const unsigned int samples_per_channel = map.size / sizeof(int16_t) / PROJECTM_STEREO; + const int16_t *data = reinterpret_cast(map.data); + projectm_pcm_add_int16(projectm_instance_, data, samples_per_channel, PROJECTM_STEREO); + } +#else + if (projectm_) { + const short samples_per_channel = static_cast(map.size) / sizeof(short) / 2; + const short *data = reinterpret_cast(map.data); + projectm_->pcm()->addPCM16Data(data, samples_per_channel); + } +#endif // HAVE_PROJECTM4 + + gst_buffer_unmap(buffer, &map); + gst_buffer_unref(buffer); + +} + +void ProjectMVisualization::SetSelected(const QStringList &paths, const bool selected) { + + for (const QString &path : paths) { + const int index = IndexOfPreset(path); + if (selected && index == -1) { +#ifdef HAVE_PROJECTM4 + projectm_playlist_add_preset(projectm_playlist_instance_, path.toUtf8().constData(), true); +#else + projectm_->addPresetURL(path.toStdString(), std::string(), default_rating_list_); +#endif + } + else if (!selected && index != -1) { +#ifdef HAVE_PROJECTM4 + projectm_playlist_remove_preset(projectm_playlist_instance_, index); +#else + projectm_->removePreset(index); +#endif + } + } + + Save(); + +} + +void ProjectMVisualization::ClearSelected() { + +#ifdef HAVE_PROJECTM4 + projectm_playlist_clear(projectm_playlist_instance_); +#else + projectm_->clearPlaylist(); +#endif + + Save(); + +} + +int ProjectMVisualization::IndexOfPreset(const QString &preset_path) const { + +#ifdef HAVE_PROJECTM4 + const uint count = projectm_playlist_size(projectm_playlist_instance_); + for (uint i = 0; i < count; ++i) { + char *projectm_preset_path = projectm_playlist_item(projectm_playlist_instance_, i); + if (projectm_preset_path) { + const QScopeGuard projectm_preset_path_deleter = qScopeGuard([projectm_preset_path](){ projectm_playlist_free_string(projectm_preset_path); }); + if (QLatin1String(projectm_preset_path) == preset_path) { + return static_cast(i); + } + } + } +#else + const uint count = projectm_->getPlaylistSize(); + for (uint i = 0; i < count; ++i) { + if (QString::fromStdString(projectm_->getPresetURL(i)) == preset_path) return static_cast(i); + } +#endif // HAVE_PROJECTM4 + + return -1; + +} + +void ProjectMVisualization::Load() { + + Settings s; + s.beginGroup(QLatin1String(VisualizationContainer::kSettingsGroup)); + mode_ = Mode(s.value("mode", 0).toInt()); + duration_ = s.value("duration", duration_).toInt(); + s.endGroup(); + +#ifdef HAVE_PROJECTM4 + projectm_set_preset_duration(projectm_instance_, duration_); + projectm_playlist_clear(projectm_playlist_instance_); +#else + projectm_->changePresetDuration(duration_); + projectm_->clearPlaylist(); +#endif // HAVE_PROJECTM4 + + switch (mode_) { + case Mode::Random:{ + for (int i = 0; i < preset_model_->all_presets_.count(); ++i) { +#ifdef HAVE_PROJECTM4 + projectm_playlist_add_preset(projectm_playlist_instance_, preset_model_->all_presets_[i].path_.toUtf8().constData(), false); +#else + projectm_->addPresetURL(preset_model_->all_presets_[i].path_.toStdString(), std::string(), default_rating_list_); +#endif + preset_model_->all_presets_[i].selected_ = true; + } + break; + } + case Mode::FromList:{ + s.beginGroup(QLatin1String(VisualizationContainer::kSettingsGroup)); + const QStringList paths = s.value("preset_paths").toStringList(); + s.endGroup(); + for (const QString &path : paths) { +#ifdef HAVE_PROJECTM4 + projectm_playlist_add_preset(projectm_playlist_instance_, path.toUtf8().constData(), true); +#else + projectm_->addPresetURL(path.toStdString(), std::string(), default_rating_list_); +#endif + preset_model_->MarkSelected(path, true); + } + } + } + +} + +void ProjectMVisualization::Save() { + + QStringList paths; + + for (const ProjectMPresetModel::Preset &preset : std::as_const(preset_model_->all_presets_)) { + if (preset.selected_) paths << preset.path_; + } + + Settings s; + s.beginGroup(VisualizationContainer::kSettingsGroup); + s.setValue("preset_paths", paths); + s.setValue("mode", static_cast(mode_)); + s.setValue("duration", duration_); + s.endGroup(); + +} + +void ProjectMVisualization::SetMode(const Mode mode) { + + mode_ = mode; + Save(); + +} + +QString ProjectMVisualization::preset_path() const { + +#ifdef HAVE_PROJECTM4 + return preset_path_; +#else + if (projectm_) { + return QString::fromStdString(projectm_->settings().presetURL); + } + return QString(); +#endif // HAVE_PROJECTM4 + +} + +void ProjectMVisualization::SetImmediatePreset(const int index) { + +#ifdef HAVE_PROJECTM4 + if (projectm_playlist_instance_) { + projectm_playlist_set_position(projectm_playlist_instance_, index, true); + } +#else + if (projectm_) { + projectm_->selectPreset(index, true); + } +#endif // HAVE_PROJECTM4 + +} + +void ProjectMVisualization::SetImmediatePreset(const QString &path) { + + const int index = IndexOfPreset(path); + if (index != -1) { + SetImmediatePreset(index); + } + +} + +void ProjectMVisualization::Lock(const bool lock) { + +#ifdef HAVE_PROJECTM4 + if (projectm_instance_) { + projectm_set_preset_locked(projectm_instance_, lock); + } +#else + if (projectm_) { + projectm_->setPresetLock(lock); + } +#endif // HAVE_PROJECTM4 + + if (!lock) Load(); + +} diff --git a/src/visualizations/projectmvisualization.h b/src/visualizations/projectmvisualization.h new file mode 100644 index 0000000000..cf862b7b7c --- /dev/null +++ b/src/visualizations/projectmvisualization.h @@ -0,0 +1,114 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef PROJECTMVISUALIZATION_H +#define PROJECTMVISUALIZATION_H + +#include "config.h" + +#include +#include + +#ifdef HAVE_PROJECTM4 +# include +# include +#else +# include +#endif + +#include +#include +#include + +#include "engine/gstbufferconsumer.h" + +class projectM; +class QPainter; +class ProjectMPresetModel; +class VisualizationContainer; + +class ProjectMVisualization : public QGraphicsScene, public GstBufferConsumer { + Q_OBJECT + + public: + explicit ProjectMVisualization(VisualizationContainer *container); + ~ProjectMVisualization(); + + enum class Mode { + Random = 0, + FromList = 1, + }; + + QString preset_path() const; + ProjectMPresetModel *preset_model() const { return preset_model_; } + + Mode mode() const { return mode_; } + int duration() const { return duration_; } + + void Init(); + + // BufferConsumer + void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) override; + + public slots: + void SetTextureSize(const int size); + void SetDuration(const int seconds); + + void SetSelected(const QStringList &paths, const bool selected); + void ClearSelected(); + void SetImmediatePreset(const int index); + void SetImmediatePreset(const QString &path); + void SetMode(const Mode mode); + + void Lock(const bool lock); + + protected: + // QGraphicsScene + void drawBackground(QPainter *painter, const QRectF &rect) override; + + private slots: + void SceneRectChanged(const QRectF &rect); + + private: + void Load(); + void Save(); + + int IndexOfPreset(const QString &preset_path) const; + + void Resize(const qreal width, const qreal height, const qreal pixel_ratio); + + private: + VisualizationContainer *container_; + ProjectMPresetModel *preset_model_; +#ifdef HAVE_PROJECTM4 + projectm_handle projectm_instance_; + projectm_playlist_handle projectm_playlist_instance_; +#else + std::unique_ptr projectm_; +#endif + Mode mode_; + int duration_; + std::vector default_rating_list_; + int texture_size_; + QString preset_path_; +}; + +#endif // PROJECTMVISUALIZATION_H diff --git a/src/visualizations/visualizationcontainer.cpp b/src/visualizations/visualizationcontainer.cpp new file mode 100644 index 0000000000..3c840feef4 --- /dev/null +++ b/src/visualizations/visualizationcontainer.cpp @@ -0,0 +1,340 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +# include +#else +# include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/iconloader.h" +#include "core/settings.h" +#include "engine/gstengine.h" +#include "visualizationcontainer.h" +#include "visualizationopenglwidget.h" +#include "visualizationoverlay.h" +#include "visualizationselector.h" +#include "projectmvisualization.h" + +const char *VisualizationContainer::kSettingsGroup = "Visualizations"; + +namespace { +constexpr int kLowFramerate = 15; +constexpr int kMediumFramerate = 25; +constexpr int kHighFramerate = 35; +constexpr int kSuperHighFramerate = 60; + +constexpr int kDefaultWidth = 828; +constexpr int kDefaultHeight = 512; +constexpr int kDefaultFps = kHighFramerate; +constexpr int kDefaultTextureSize = 512; +} // namespace + +VisualizationContainer::VisualizationContainer(QWidget *parent) + : QGraphicsView(parent), + projectm_visualization_(new ProjectMVisualization(this)), + overlay_(new VisualizationOverlay), + selector_(new VisualizationSelector(this)), + overlay_proxy_(nullptr), + engine_(nullptr), + menu_(new QMenu(this)), + fps_(kDefaultFps), + size_(kDefaultTextureSize) { + + setWindowTitle(tr("Visualizations")); + setWindowIcon(IconLoader::Load(QStringLiteral("strawberry"))); + setMinimumSize(64, 64); + + { + Settings s; + s.beginGroup(QLatin1String(kSettingsGroup)); + if (!restoreGeometry(s.value("geometry").toByteArray())) { + resize(kDefaultWidth, kDefaultHeight); + } + fps_ = s.value("fps", kDefaultFps).toInt(); + size_ = s.value("size", kDefaultTextureSize).toInt(); + s.endGroup(); + } + + QShortcut *close = new QShortcut(QKeySequence::Close, this); + QObject::connect(close, &QShortcut::activated, this, &VisualizationContainer::close); + + // Set up the graphics view + setScene(projectm_visualization_); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + setViewport(new VisualizationOpenGLWidget(projectm_visualization_)); +#else + setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); +#endif + setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setFrameStyle(QFrame::NoFrame); + + // Add the overlay + overlay_proxy_ = scene()->addWidget(overlay_); + QObject::connect(overlay_, &VisualizationOverlay::OpacityChanged, this, &VisualizationContainer::ChangeOverlayOpacity); + QObject::connect(overlay_, &VisualizationOverlay::ShowPopupMenu, this, &VisualizationContainer::ShowPopupMenu); + ChangeOverlayOpacity(0.0); + + projectm_visualization_->SetTextureSize(size_); + SizeChanged(); + + // Selector + selector_->SetVisualization(projectm_visualization_); + + // Settings menu + menu_->addAction(IconLoader::Load(QStringLiteral("view-fullscreen")), tr("Toggle fullscreen"), this, &VisualizationContainer::ToggleFullscreen); + + QMenu *fps_menu = menu_->addMenu(tr("Framerate")); + QActionGroup *fps_group = new QActionGroup(this); + AddFramerateMenuItem(tr("Low (%1 fps)").arg(kLowFramerate), kLowFramerate, fps_, fps_group); + AddFramerateMenuItem(tr("Medium (%1 fps)").arg(kMediumFramerate), kMediumFramerate, fps_, fps_group); + AddFramerateMenuItem(tr("High (%1 fps)").arg(kHighFramerate), kHighFramerate, fps_, fps_group); + AddFramerateMenuItem(tr("Super high (%1 fps)").arg(kSuperHighFramerate), kSuperHighFramerate, fps_, fps_group); + fps_menu->addActions(fps_group->actions()); + + QMenu *quality_menu = menu_->addMenu(tr("Quality", "Visualization quality")); + QActionGroup *quality_group = new QActionGroup(this); + AddQualityMenuItem(tr("Low (256x256)"), 256, size_, quality_group); + AddQualityMenuItem(tr("Medium (512x512)"), 512, size_, quality_group); + AddQualityMenuItem(tr("High (1024x1024)"), 1024, size_, quality_group); + AddQualityMenuItem(tr("Super high (2048x2048)"), 2048, size_, quality_group); + quality_menu->addActions(quality_group->actions()); + + menu_->addAction(tr("Select visualizations..."), selector_, &VisualizationContainer::show); + + menu_->addSeparator(); + menu_->addAction(IconLoader::Load(QStringLiteral("application-exit")), tr("Close visualization"), this, &VisualizationContainer::hide); + +} + +void VisualizationContainer::AddFramerateMenuItem(const QString &name, const int value, const int def, QActionGroup *group) { + + QAction *action = group->addAction(name); + action->setCheckable(true); + action->setChecked(value == def); + QObject::connect(action, &QAction::triggered, this, [this, value]() { SetFps(value); }); + +} + +void VisualizationContainer::AddQualityMenuItem(const QString &name, const int value, const int def, QActionGroup *group) { + + QAction *action = group->addAction(name); + action->setCheckable(true); + action->setChecked(value == def); + QObject::connect(action, &QAction::triggered, this, [this, value]() { SetQuality(value); }); + +} + +void VisualizationContainer::SetEngine(GstEngine *engine) { + + engine_ = engine; + + if (isVisible()) engine_->AddBufferConsumer(projectm_visualization_); + +} + +void VisualizationContainer::showEvent(QShowEvent *e) { + + qLog(Debug) << "Showing visualization"; + + QGraphicsView::showEvent(e); + + update_timer_.start(1000 / fps_, this); + + if (engine_) engine_->AddBufferConsumer(projectm_visualization_); + +} + +void VisualizationContainer::hideEvent(QHideEvent *e) { + + qLog(Debug) << "Hiding visualization"; + + QGraphicsView::hideEvent(e); + + update_timer_.stop(); + + if (engine_) engine_->RemoveBufferConsumer(projectm_visualization_); + +} + +void VisualizationContainer::closeEvent(QCloseEvent *e) { + + Q_UNUSED(e); + + // Don't close the window. Just hide it. + e->ignore(); + hide(); + +} + +void VisualizationContainer::resizeEvent(QResizeEvent *e) { + QGraphicsView::resizeEvent(e); + SizeChanged(); +} + +void VisualizationContainer::SizeChanged() { + + // Save the geometry + Settings s; + s.beginGroup(kSettingsGroup); + s.setValue("geometry", saveGeometry()); + + // Resize the scene + if (scene()) scene()->setSceneRect(QRect(QPoint(0, 0), size())); + + // Resize the overlay + if (overlay_) overlay_->resize(size()); + +} + +void VisualizationContainer::timerEvent(QTimerEvent *e) { + + QGraphicsView::timerEvent(e); + if (e->timerId() == update_timer_.timerId()) scene()->update(); + +} + +void VisualizationContainer::SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next) { + overlay_->SetActions(previous, play_pause, stop, next); +} + +void VisualizationContainer::SongMetadataChanged(const Song &metadata) { + overlay_->SetSongTitle(QStringLiteral("%1 - %2").arg(metadata.artist(), metadata.title())); +} + +void VisualizationContainer::Stopped() { + overlay_->SetSongTitle(tr("strawberry")); +} + +void VisualizationContainer::ChangeOverlayOpacity(const qreal value) { + + overlay_proxy_->setOpacity(value); + + // Hide the cursor if the overlay is hidden + if (value < 0.5) { + viewport()->setCursor(Qt::BlankCursor); + } + else { + viewport()->unsetCursor(); + } + + +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +void VisualizationContainer::enterEvent(QEnterEvent *e) { +#else +void VisualizationContainer::enterEvent(QEvent *e) { +#endif + + QGraphicsView::enterEvent(e); + + overlay_->SetVisible(true); + +} + +void VisualizationContainer::leaveEvent(QEvent *e) { + QGraphicsView::leaveEvent(e); + overlay_->SetVisible(false); +} + +void VisualizationContainer::mouseMoveEvent(QMouseEvent *e) { + QGraphicsView::mouseMoveEvent(e); + overlay_->SetVisible(true); +} + +void VisualizationContainer::mouseDoubleClickEvent(QMouseEvent *e) { + QGraphicsView::mouseDoubleClickEvent(e); + ToggleFullscreen(); +} + +void VisualizationContainer::contextMenuEvent(QContextMenuEvent *event) { + QGraphicsView::contextMenuEvent(event); + ShowPopupMenu(event->pos()); +} + +void VisualizationContainer::keyReleaseEvent(QKeyEvent *event) { + + if (event->matches(QKeySequence::Close) || event->key() == Qt::Key_Escape) { + if (isFullScreen()) { + ToggleFullscreen(); + } + else { + hide(); + } + return; + } + + QGraphicsView::keyReleaseEvent(event); + +} + +void VisualizationContainer::ToggleFullscreen() { + + setWindowState(windowState() ^ Qt::WindowFullScreen); + +} + +void VisualizationContainer::SetFps(const int fps) { + + fps_ = fps; + + // Save settings + Settings s; + s.beginGroup(kSettingsGroup); + s.setValue("fps", fps_); + + update_timer_.stop(); + update_timer_.start(1000 / fps_, this); + +} + +void VisualizationContainer::ShowPopupMenu(const QPoint &pos) { + menu_->popup(mapToGlobal(pos)); +} + +void VisualizationContainer::SetQuality(const int size) { + + size_ = size; + + // Save settings + Settings s; + s.beginGroup(kSettingsGroup); + s.setValue("size", size_); + + projectm_visualization_->SetTextureSize(size_); + +} diff --git a/src/visualizations/visualizationcontainer.h b/src/visualizations/visualizationcontainer.h new file mode 100644 index 0000000000..e5ee5254d0 --- /dev/null +++ b/src/visualizations/visualizationcontainer.h @@ -0,0 +1,109 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef VISUALIZATIONCONTAINER_H +#define VISUALIZATIONCONTAINER_H + +#include "config.h" + +#include +#include + +#include "core/song.h" + +class GstEngine; +class ProjectMVisualization; +class VisualizationOverlay; +class VisualizationSelector; + +class QMenu; +class QActionGroup; +class QEvent; +class QShowEvent; +class QHideEvent; +class QCloseEvent; +class QResizeEvent; +class QTimerEvent; +class QMouseEvent; +class QContextMenuEvent; +class QKeyEvent; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +class QEnterEvent; +#endif + +class VisualizationContainer : public QGraphicsView { + Q_OBJECT + + public: + explicit VisualizationContainer(QWidget *parent = nullptr); + + static const char *kSettingsGroup; + + void SetEngine(GstEngine *engine); + void SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next); + + public slots: + void SongMetadataChanged(const Song &metadata); + void Stopped(); + + protected: + // QWidget + void showEvent(QShowEvent *e) override; + void hideEvent(QHideEvent *e) override; + void closeEvent(QCloseEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void timerEvent(QTimerEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + void enterEvent(QEnterEvent *e) override; +#else + void enterEvent(QEvent *e) override; +#endif + void leaveEvent(QEvent *e) override; + void mouseDoubleClickEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + + private: + void SizeChanged(); + void AddFramerateMenuItem(const QString &name, int value, int def, QActionGroup *group); + void AddQualityMenuItem(const QString &name, int value, int def, QActionGroup *group); + + private slots: + void ChangeOverlayOpacity(qreal value); + void ShowPopupMenu(const QPoint &pos); + void ToggleFullscreen(); + void SetFps(const int fps); + void SetQuality(const int size); + + private: + ProjectMVisualization *projectm_visualization_; + VisualizationOverlay *overlay_; + VisualizationSelector *selector_; + QGraphicsProxyWidget *overlay_proxy_; + GstEngine *engine_; + QMenu *menu_; + QBasicTimer update_timer_; + int fps_; + int size_; +}; + +#endif // VISUALIZATIONCONTAINER_H diff --git a/src/visualizations/visualizationopenglwidget.cpp b/src/visualizations/visualizationopenglwidget.cpp new file mode 100644 index 0000000000..74ce4a90b9 --- /dev/null +++ b/src/visualizations/visualizationopenglwidget.cpp @@ -0,0 +1,37 @@ +/* + * Strawberry Music Player + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include "visualizationopenglwidget.h" +#include "projectmvisualization.h" + +VisualizationOpenGLWidget::VisualizationOpenGLWidget(ProjectMVisualization *projectm_visualization, QWidget *parent, Qt::WindowFlags f) + : QOpenGLWidget(parent, f), + projectm_visualization_(projectm_visualization) {} + +void VisualizationOpenGLWidget::initializeGL() { + + projectm_visualization_->Init(); + + QOpenGLWidget::initializeGL(); + +} diff --git a/src/visualizations/visualizationopenglwidget.h b/src/visualizations/visualizationopenglwidget.h new file mode 100644 index 0000000000..2fc086ecc5 --- /dev/null +++ b/src/visualizations/visualizationopenglwidget.h @@ -0,0 +1,42 @@ +/* + * Strawberry Music Player + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef VISUALIZATIONOPENGLWIDGET_H +#define VISUALIZATIONOPENGLWIDGET_H + +#include "config.h" + +#include + +class ProjectMVisualization; + +class VisualizationOpenGLWidget : public QOpenGLWidget { + Q_OBJECT + + public: + explicit VisualizationOpenGLWidget(ProjectMVisualization *projectm_visualization, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + + protected: + void initializeGL() override; + + private: + ProjectMVisualization *projectm_visualization_; +}; + +#endif // VISUALIZATIONOPENGLWIDGET_H diff --git a/src/visualizations/visualizationoverlay.cpp b/src/visualizations/visualizationoverlay.cpp new file mode 100644 index 0000000000..b31cae870d --- /dev/null +++ b/src/visualizations/visualizationoverlay.cpp @@ -0,0 +1,116 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "core/iconloader.h" +#include "visualizationoverlay.h" +#include "ui_visualizationoverlay.h" + +namespace { +constexpr int kFadeDuration = 500; +constexpr int kFadeTimeout = 5000; +} + +VisualizationOverlay::VisualizationOverlay(QWidget *parent) + : QWidget(parent), + ui_(new Ui_VisualizationOverlay), + fade_timeline_(new QTimeLine(kFadeDuration, this)), + visible_(false) { + + ui_->setupUi(this); + + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + ui_->settings->setIcon(IconLoader::Load(QStringLiteral("configure"))); + + QObject::connect(ui_->settings, &QToolButton::clicked, this, &VisualizationOverlay::ShowSettingsMenu); + QObject::connect(fade_timeline_, &QTimeLine::valueChanged, this, &VisualizationOverlay::OpacityChanged); + +} + +VisualizationOverlay::~VisualizationOverlay() { delete ui_; } + +QGraphicsProxyWidget *VisualizationOverlay::title(QGraphicsProxyWidget *proxy) const { + return proxy->createProxyForChildWidget(ui_->song_title); +} + +void VisualizationOverlay::SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next) { + + ui_->previous->setDefaultAction(previous); + ui_->play_pause->setDefaultAction(play_pause); + ui_->stop->setDefaultAction(stop); + ui_->next->setDefaultAction(next); + +} + +void VisualizationOverlay::ShowSettingsMenu() { + + emit ShowPopupMenu(ui_->settings->mapToGlobal(ui_->settings->rect().bottomLeft())); + +} + +void VisualizationOverlay::timerEvent(QTimerEvent *e) { + + QWidget::timerEvent(e); + + if (e->timerId() == fade_out_timeout_.timerId()) { + SetVisible(false); + } + +} + +void VisualizationOverlay::SetVisible(const bool visible) { + + // If we're showing the overlay, then fade out again in a little while + fade_out_timeout_.stop(); + if (visible) fade_out_timeout_.start(kFadeTimeout, this); + + // Don't change to the state we're in already + if (visible == visible_) return; + visible_ = visible; + + // If there's already another fader running then start from the same time that one was already at. + int start_time = visible ? 0 : fade_timeline_->duration(); + if (fade_timeline_->state() == QTimeLine::Running) + start_time = fade_timeline_->currentTime(); + + fade_timeline_->stop(); + fade_timeline_->setDirection(visible ? QTimeLine::Forward : QTimeLine::Backward); + fade_timeline_->setCurrentTime(start_time); + fade_timeline_->resume(); + +} + +void VisualizationOverlay::SetSongTitle(const QString &title) { + + ui_->song_title->setText(title); + SetVisible(true); + +} diff --git a/src/visualizations/visualizationoverlay.h b/src/visualizations/visualizationoverlay.h new file mode 100644 index 0000000000..09bfb4bf49 --- /dev/null +++ b/src/visualizations/visualizationoverlay.h @@ -0,0 +1,71 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef VISUALIZATIONOVERLAY_H +#define VISUALIZATIONOVERLAY_H + +#include "config.h" + +#include +#include +#include + +class Ui_VisualizationOverlay; + +class QGraphicsProxyWidget; +class QTimeLine; +class QAction; + +class VisualizationOverlay : public QWidget { + Q_OBJECT + + public: + explicit VisualizationOverlay(QWidget *parent = nullptr); + ~VisualizationOverlay(); + + QGraphicsProxyWidget *title(QGraphicsProxyWidget *proxy) const; + + void SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next); + void SetSongTitle(const QString &title); + + public slots: + void SetVisible(const bool visible); + + signals: + void OpacityChanged(const qreal value); + void ShowPopupMenu(const QPoint &pos); + + protected: + // QWidget + void timerEvent(QTimerEvent *e); + + private slots: + void ShowSettingsMenu(); + + private: + Ui_VisualizationOverlay *ui_; + + QTimeLine *fade_timeline_; + QBasicTimer fade_out_timeout_; + bool visible_; +}; + +#endif // VISUALIZATIONOVERLAY_H diff --git a/src/visualizations/visualizationoverlay.ui b/src/visualizations/visualizationoverlay.ui new file mode 100644 index 0000000000..dfd985619c --- /dev/null +++ b/src/visualizations/visualizationoverlay.ui @@ -0,0 +1,234 @@ + + + VisualizationOverlay + + + + 0 + 0 + 523 + 302 + + + + Form + + + VisualizationOverlay { + background-color: transparent; +} + +#frame { + background-color: rgba(96, 59, 25, 70%); + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border-color: rgba(145, 89, 38, 100%); + border-width: 4px 4px 0px 4px; + border-style: solid; +} + +#song_title { + font-weight: bold; + font-size: 20px; + color: #feae65; +} + +QToolButton { + background: transparent; + border: none; +} + + + + 0 + + + + + Qt::Vertical + + + + 20 + 210 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Strawberry + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + + + + + + 24 + 24 + + + + + + + + + 24 + 24 + + + + + + + + + 24 + 24 + + + + + + + + + 24 + 24 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 13 + + + + + + + + Visualizations Settings + + + + 24 + 24 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/src/visualizations/visualizationselector.cpp b/src/visualizations/visualizationselector.cpp new file mode 100644 index 0000000000..c8e06394d5 --- /dev/null +++ b/src/visualizations/visualizationselector.cpp @@ -0,0 +1,88 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include + +#include "visualizationselector.h" +#include "projectmpresetmodel.h" +#include "projectmvisualization.h" +#include "ui_visualizationselector.h" + +VisualizationSelector::VisualizationSelector(QWidget *parent) + : QDialog(parent), + ui_(new Ui_VisualizationSelector), + projectm_visualization_(nullptr), + select_all_(nullptr), + select_none_(nullptr) { + + ui_->setupUi(this); + + select_all_ = ui_->buttonBox->addButton(tr("Select All"), QDialogButtonBox::ActionRole); + select_none_ = ui_->buttonBox->addButton(tr("Select None"), QDialogButtonBox::ActionRole); + QObject::connect(select_all_, &QPushButton::clicked, this, &VisualizationSelector::SelectAll); + QObject::connect(select_none_, &QPushButton::clicked, this, &VisualizationSelector::SelectNone); + select_all_->setEnabled(false); + select_none_->setEnabled(false); + + QObject::connect(ui_->mode, QOverload::of(&QComboBox::currentIndexChanged), this, &VisualizationSelector::ModeChanged); + +} + +VisualizationSelector::~VisualizationSelector() { delete ui_; } + +void VisualizationSelector::showEvent(QShowEvent *e) { + + Q_UNUSED(e); + + if (!ui_->list->model()) { + ui_->delay->setValue(projectm_visualization_->duration()); + ui_->list->setModel(projectm_visualization_->preset_model()); + QObject::connect(ui_->list->selectionModel(), &QItemSelectionModel::currentChanged, projectm_visualization_->preset_model(), &ProjectMPresetModel::SetImmediatePreset); + QObject::connect(ui_->delay, QOverload::of(&QSpinBox::valueChanged), projectm_visualization_, &ProjectMVisualization::SetDuration); + + ui_->mode->setCurrentIndex(static_cast(projectm_visualization_->mode())); + } + + projectm_visualization_->Lock(true); + +} + +void VisualizationSelector::hideEvent(QHideEvent *e) { + Q_UNUSED(e); + projectm_visualization_->Lock(false); +} + +void VisualizationSelector::ModeChanged(const int mode) { + + const bool enabled = mode == 1; + ui_->list->setEnabled(enabled); + select_all_->setEnabled(enabled); + select_none_->setEnabled(enabled); + + projectm_visualization_->SetMode(static_cast(mode)); + +} + +void VisualizationSelector::SelectAll() { projectm_visualization_->preset_model()->SelectAll(); } + +void VisualizationSelector::SelectNone() { projectm_visualization_->preset_model()->SelectNone(); } diff --git a/src/visualizations/visualizationselector.h b/src/visualizations/visualizationselector.h new file mode 100644 index 0000000000..12561fc8dd --- /dev/null +++ b/src/visualizations/visualizationselector.h @@ -0,0 +1,61 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef VISUALIZATIONSELECTOR_H +#define VISUALIZATIONSELECTOR_H + +#include "config.h" + +#include + +class QPushButton; +class QShowEvent; +class QHideEvent; + +class ProjectMVisualization; +class Ui_VisualizationSelector; + +class VisualizationSelector : public QDialog { + Q_OBJECT + + public: + explicit VisualizationSelector(QWidget *parent = nullptr); + ~VisualizationSelector(); + + void SetVisualization(ProjectMVisualization *projectm_visualization) { projectm_visualization_ = projectm_visualization; } + + protected: + void showEvent(QShowEvent *e) override; + void hideEvent(QHideEvent *e) override; + + private slots: + void ModeChanged(const int mode); + void SelectAll(); + void SelectNone(); + + private: + Ui_VisualizationSelector *ui_; + ProjectMVisualization *projectm_visualization_; + QPushButton *select_all_; + QPushButton *select_none_; +}; + +#endif // VISUALIZATIONSELECTOR_H diff --git a/src/visualizations/visualizationselector.ui b/src/visualizations/visualizationselector.ui new file mode 100644 index 0000000000..1c9a071d54 --- /dev/null +++ b/src/visualizations/visualizationselector.ui @@ -0,0 +1,140 @@ + + + VisualizationSelector + + + + 0 + 0 + 595 + 475 + + + + Select visualizations + + + + :/icon.png:/icon.png + + + + + + + + Visualization mode + + + + + + + + Random visualization + + + + + Choose from the list + + + + + + + + Delay between visualizations + + + + + + + seconds + + + 2 + + + 120 + + + 15 + + + + + + + + + false + + + true + + + QAbstractItemView::SelectRows + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + mode + delay + list + buttonBox + + + + + + + buttonBox + accepted() + VisualizationSelector + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + VisualizationSelector + reject() + + + 316 + 260 + + + 286 + 274 + + + + +