Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ef0065a49c MainWindow: Save tab mode immediately when changed
The sidebar mode (tabs on top, large sidebar, etc.) was only being saved
when the application exits normally. This caused the setting to be lost
if the application crashed or was force-closed, reverting to the default
"Large Sidebar" mode on next startup.

This fix connects the FancyTabWidget::ModeChanged signal to immediately
save the tab_mode setting when the user changes it via the context menu.

Fixes issue where sidebar keeps periodically reverting to Large mode.

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 15:34:55 +00:00
copilot-swe-agent[bot]
28fb5a863c Initial plan 2026-01-03 15:12:21 +00:00
Jonas Kvinge
e8d9e1172f FileViewTreeModel: Add const 2026-01-03 16:09:56 +01:00
Alexopus
aac8d4e68b Add file tree view 2026-01-03 15:11:56 +01:00
dependabot[bot]
0e28e800b3 Bump vmactions/freebsd-vm from 1.3.4 to 1.3.5
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 17:33:02 +01:00
Jonas Kvinge
cf84bc29ab CI: Manually codesign 2026-01-01 01:51:10 +01:00
Jonas Kvinge
afc3effc9d CI: Switch macOS dependencies repo 2025-12-30 20:01:34 +01:00
Jonas Kvinge
370bebff5f CollectionView: Fix Enter/Return behavior to respect double-click settings
Fixes #1691
2025-12-30 19:08:52 +01:00
Jonas Kvinge
db410cc257 MainWindow: Remove unused declaration 2025-12-29 22:14:08 +01:00
Jonas Kvinge
20a9946e51 Song: Prefer filenames with "front" or "cover" for art automatic
Fixes #1745
2025-12-29 21:16:06 +01:00
dependabot[bot]
b6c8ff19af Bump vmactions/freebsd-vm from 1.3.2 to 1.3.4
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.2 to 1.3.4.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.2...v1.3.4)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 18:18:55 +01:00
dependabot[bot]
80d058af10 Bump vmactions/openbsd-vm from 1.2.9 to 1.3.1
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.2.9 to 1.3.1.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.2.9...v1.3.1)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:23:16 +01:00
39 changed files with 1073 additions and 2219 deletions

View File

@@ -62,7 +62,6 @@ jobs:
libchromaprint-devel
fftw3-devel
libebur128-devel
projectM-devel
desktop-file-utils
update-desktop-files
appstream-glib
@@ -79,7 +78,6 @@ jobs:
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
qt6-openglwidgets-devel
gtest
gmock
sparsehash-devel
@@ -202,7 +200,6 @@ jobs:
libchromaprint-devel
libebur128-devel
fftw-devel
libprojectM-devel
desktop-file-utils
libappstream-glib
hicolor-icon-theme
@@ -293,7 +290,6 @@ jobs:
lib64Qt6DBus-devel
lib64Qt6Gui-devel
lib64Qt6Widgets-devel
lib64Qt6OpenGLWidgets-devel
lib64Qt6Test-devel
lib64kdsingleapplication-devel
lib64xkbcommon-devel
@@ -388,7 +384,6 @@ jobs:
lib64fftw-devel
lib64dbus-devel
lib64appstream-devel
lib64projectm-devel
lib64qt6core-devel
lib64qt6gui-devel
lib64qt6widgets-devel
@@ -398,7 +393,6 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64qt6openglwidgets-devel
lib64sparsehash-devel
lib64kdsingleapplication-devel
desktop-file-utils
@@ -505,7 +499,6 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.debian_version != 'bookworm'
run: apt install -y libkdsingleapplication-qt6-dev
@@ -602,7 +595,6 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
run: apt install -y libkdsingleapplication-qt6-dev
@@ -701,7 +693,6 @@ jobs:
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
libprojectm-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
@@ -756,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.2
uses: vmactions/freebsd-vm@v1.3.5
with:
usesh: true
mem: 8192
@@ -781,7 +772,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.9
uses: vmactions/openbsd-vm@v1.3.1
with:
usesh: true
mem: 4096
@@ -854,7 +845,7 @@ jobs:
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
- name: Download macOS dependencies
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
- name: Extract macOS dependencies
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
@@ -907,7 +898,7 @@ jobs:
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'

View File

@@ -208,15 +208,6 @@ else()
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
endif()
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)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
@@ -227,7 +218,7 @@ set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS GuiPrivate OpenGLWidgets LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -398,11 +389,6 @@ if(HAVE_X11_GLOBALSHORTCUTS OR HAVE_KGLOBALACCEL_GLOBALSHORTCUTS OR APPLE OR WIN
set(HAVE_GLOBALSHORTCUTS ON)
endif()
optional_component(VISUALIZATIONS ON "Visualizations"
DEPENDS "libprojectm" LIBPROJECTM_FOUND
DEPENDS "QtOpenGLWidgets" Qt${QT_VERSION_MAJOR}OpenGLWidgets_FOUND
)
if(NOT CMAKE_CROSSCOMPILING)
# Check that we have Qt with sqlite driver
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
@@ -837,6 +823,8 @@ set(SOURCES
src/fileview/fileview.cpp
src/fileview/fileviewlist.cpp
src/fileview/fileviewtree.cpp
src/fileview/fileviewtreemodel.cpp
src/device/devicemanager.cpp
src/device/devicelister.cpp
@@ -1126,6 +1114,8 @@ set(HEADERS
src/fileview/fileview.h
src/fileview/fileviewlist.h
src/fileview/fileviewtree.h
src/fileview/fileviewtreemodel.h
src/device/devicemanager.h
src/device/devicelister.h
@@ -1494,26 +1484,6 @@ optional_source(HAVE_QOBUZ
src/settings/qobuzsettingspage.ui
)
optional_source(HAVE_VISUALIZATIONS
SOURCES
src/visualizations/projectmpresetmodel.cpp
src/visualizations/projectmvisualization.cpp
src/visualizations/visualizationcontainer.cpp
src/visualizations/visualizationoverlay.cpp
src/visualizations/visualizationselector.cpp
src/visualizations/visualizationopenglwidget.cpp
HEADERS
src/visualizations/projectmpresetmodel.h
src/visualizations/projectmvisualization.h
src/visualizations/visualizationcontainer.h
src/visualizations/visualizationoverlay.h
src/visualizations/visualizationselector.h
src/visualizations/visualizationopenglwidget.h
UI
src/visualizations/visualizationoverlay.ui
src/visualizations/visualizationselector.ui
)
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
@@ -1584,7 +1554,6 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:Qt${QT_VERSION_MAJOR}::OpenGLWidgets>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
@@ -1600,7 +1569,6 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:${LIBPROJECTM_LIBRARIES}>
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication

3
debian/control vendored
View File

@@ -32,8 +32,7 @@ Build-Depends: debhelper-compat (= 12),
libfftw3-dev,
libebur128-dev,
libsparsehash-dev,
rapidjson-dev,
libprojectm-dev
rapidjson-dev
Standards-Version: 4.7.0
Package: strawberry

View File

@@ -43,7 +43,6 @@ 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)

View File

@@ -73,8 +73,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
double_click_timer_(new QTimer(this)),
ignore_next_click_(false),
current_analyzer_(nullptr),
engine_(nullptr),
action_visualization_(nullptr) {
engine_(nullptr) {
QHBoxLayout *layout = new QHBoxLayout(this);
setLayout(layout);
@@ -119,17 +118,6 @@ 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_);
}
@@ -261,10 +249,3 @@ 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_);
}

View File

@@ -46,7 +46,6 @@ class AnalyzerContainer : public QWidget {
explicit AnalyzerContainer(QWidget *parent);
void SetEngine(SharedPtr<EngineBase> engine);
void SetVisualizationsAction(QAction *visualization);
static const char *kSettingsGroup;
static const char *kSettingsFramerate;
@@ -56,7 +55,6 @@ class AnalyzerContainer : public QWidget {
protected:
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private Q_SLOTS:
@@ -91,8 +89,6 @@ class AnalyzerContainer : public QWidget {
AnalyzerBase *current_analyzer_;
SharedPtr<EngineBase> engine_;
QAction *action_visualization_;
};
template<typename T>

View File

@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
case Qt::Key_Enter:
case Qt::Key_Return:
if (currentIndex().isValid()) {
AddToPlaylist();
Q_EMIT doubleClicked(currentIndex());
}
e->accept();
break;

View File

@@ -48,7 +48,4 @@
#cmakedefine ENABLE_WIN32_CONSOLE
#cmakedefine HAVE_VISUALIZATIONS
#cmakedefine HAVE_PROJECTM4
#endif // CONFIG_H_IN

View File

@@ -206,11 +206,6 @@
#include "organize/organizeerrordialog.h"
#ifdef HAVE_VISUALIZATIONS
# include "visualizations/visualizationcontainer.h"
# include "engine/gstengine.h"
#endif
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
#endif
@@ -452,6 +447,14 @@ MainWindow::MainWindow(Application *app,
ui_->tabs->SetBackgroundPixmap(QPixmap(u":/pictures/sidebar-background.png"_s));
ui_->tabs->LoadSettings(QLatin1String(MainWindowSettings::kSettingsGroup));
// Save tab mode immediately when changed to avoid losing the setting
QObject::connect(ui_->tabs, &FancyTabWidget::ModeChanged, this, [this](FancyTabWidget::Mode mode) {
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue("tab_mode", static_cast<int>(mode));
s.endGroup();
});
track_position_timer_->setInterval(kTrackPositionUpdateTimeMs);
QObject::connect(track_position_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackPosition);
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
@@ -628,12 +631,6 @@ MainWindow::MainWindow(Application *app,
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);
@@ -918,7 +915,6 @@ MainWindow::MainWindow(Application *app,
// 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(u"WW selected of WW tracks - [ WW:WW ]"_s));
@@ -3416,24 +3412,3 @@ 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<GstEngine*>(&*app_->player()->engine()));
}
visualization_->show();
#endif // HAVE_VISUALIZATIONS
}

View File

@@ -43,7 +43,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QtEvents>
@@ -97,7 +96,6 @@ class Windows7ThumbBar;
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
class VisualizationContainer;
#ifdef HAVE_DISCORD_RPC
namespace discord {
@@ -282,7 +280,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void ShowVisualizations();
private:
void SaveSettings();
@@ -292,9 +289,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void CheckFullRescanRevisions();
// creates the icon by painting the full one depending on the current position
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
void GetCoverAutomatically();
void SetToggleScrobblingIcon(const bool value);
@@ -362,10 +356,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
LastFMImportDialog *lastfm_import_dialog_;
#ifdef HAVE_VISUALIZATIONS
ScopedPtr<VisualizationContainer> visualization_;
#endif
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
QAction *collection_show_untagged_;

View File

@@ -517,7 +517,6 @@
<addaction name="action_cover_manager"/>
<addaction name="action_equalizer"/>
<addaction name="action_transcoder"/>
<addaction name="action_visualizations"/>
<addaction name="separator"/>
<addaction name="action_update_collection"/>
<addaction name="action_full_collection_scan"/>
@@ -864,11 +863,6 @@
<string>Import data from last.fm...</string>
</property>
</action>
<action name="action_visualizations">
<property name="text">
<string>Visualizations</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@@ -1668,12 +1668,24 @@ void Song::InitArtManual() {
void Song::InitArtAutomatic() {
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
// Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
const QFileInfo fileinfo(d->url_.toLocalFile());
const QDir dir(fileinfo.path());
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
QString best_cover_file;
for (const QString &cover_file : cover_files) {
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
continue;
}
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
best_cover_file = cover_file;
break;
}
if (best_cover_file.isEmpty()) {
best_cover_file = cover_file;
}
}
if (!best_cover_file.isEmpty()) {
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
}
}

View File

@@ -36,7 +36,7 @@ class GstBufferConsumer {
// This is called in some unspecified GStreamer thread.
// Ownership of the buffer is transferred to the BufferConsumer, and it should gst_buffer_unref it.
virtual void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) = 0;
virtual void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) = 0;
private:
Q_DISABLE_COPY(GstBufferConsumer)

View File

@@ -543,9 +543,7 @@ void GstEngine::ReloadSettings() {
}
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) {
Q_UNUSED(channels);
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) {
// Schedule this to run in the GUI thread. The buffer gets added to the queue and unreffed by UpdateScope.
if (!QMetaObject::invokeMethod(this, "AddBufferToScope", Q_ARG(GstBuffer*, buffer), Q_ARG(int, pipeline_id), Q_ARG(QString, format))) {

View File

@@ -84,7 +84,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
bool ALSADeviceSupport(const QString &output) const override;
bool ExclusiveModeSupport(const QString &output) const override;
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) override;
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) override;
public Q_SLOTS:
void ReloadSettings() override;

View File

@@ -1385,7 +1385,7 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb
for (GstBufferConsumer *consumer : std::as_const(consumers)) {
gst_buffer_ref(buf);
consumer->ConsumeBuffer(buf, instance->id(), format, channels);
consumer->ConsumeBuffer(buf, instance->id(), format);
}
if (buf16) {

View File

@@ -29,13 +29,17 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSettings>
#include <QStandardPaths>
#include <QMessageBox>
#include <QScrollBar>
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <QSpacerItem>
#include <QtEvents>
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
#include "includes/shared_ptr.h"
#include "core/deletefiles.h"
#include "core/filesystemmusicstorage.h"
@@ -45,10 +49,11 @@
#include "dialogs/deleteconfirmationdialog.h"
#include "fileview.h"
#include "fileviewlist.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
#include "ui_fileview.h"
#include "organize/organizeerrordialog.h"
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -57,9 +62,12 @@ FileView::FileView(QWidget *parent)
: QWidget(parent),
ui_(new Ui_FileView),
model_(nullptr),
tree_model_(nullptr),
undo_stack_(new QUndoStack(this)),
task_manager_(nullptr),
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) {
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
tree_view_active_(false),
view_mode_spacer_(nullptr) {
ui_->setupUi(this);
@@ -68,12 +76,14 @@ FileView::FileView(QWidget *parent)
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s));
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode);
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
@@ -87,6 +97,22 @@ FileView::FileView(QWidget *parent)
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
// Connect tree view signals
QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist);
QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection);
QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection);
QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice);
QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete);
QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags);
QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated);
QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick);
// Setup tree root management buttons
ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s));
ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s));
QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked);
QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked);
QString filter = QLatin1String(kFileFilter);
filter_list_ << filter.split(u' ');
@@ -109,6 +135,19 @@ void FileView::ReloadSettings() {
ui_->forward->setIconSize(QSize(iconsize, iconsize));
ui_->home->setIconSize(QSize(iconsize, iconsize));
ui_->up->setIconSize(QSize(iconsize, iconsize));
ui_->toggle_view->setIconSize(QSize(iconsize, iconsize));
ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize));
ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize));
// Load tree root paths setting
Settings file_settings;
file_settings.beginGroup(u"FileView"_s);
tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList();
tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool();
file_settings.endGroup();
// Set initial view mode
UpdateViewModeUI();
}
@@ -180,24 +219,46 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
}
void FileView::ItemActivated(const QModelIndex &idx) {
if (model_->isDir(idx))
// Only handle activation for list view (not tree view)
if (!tree_view_active_ && model_->isDir(idx)) {
ChangeFilePath(model_->filePath(idx));
}
}
void FileView::ItemDoubleClick(const QModelIndex &idx) {
if (model_->isDir(idx)) {
return;
QString file_path;
bool is_file = false;
// Handle tree view with virtual roots
if (tree_view_active_ && tree_model_) {
QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type);
if (type_var.isValid()) {
FileViewTreeItem::Type item_type = type_var.value<FileViewTreeItem::Type>();
// Only handle files, ignore directories and virtual roots
if (item_type == FileViewTreeItem::Type::File) {
file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString();
is_file = true;
}
}
}
// Handle list view with filesystem model
else if (!tree_view_active_ && model_) {
if (!model_->isDir(idx)) {
file_path = model_->filePath(idx);
is_file = true;
}
}
QString file_path = model_->filePath(idx);
// Add file to playlist if it's a valid file
if (is_file && !file_path.isEmpty()) {
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
Q_EMIT AddToPlaylist(mimedata);
}
}
@@ -272,12 +333,156 @@ void FileView::showEvent(QShowEvent *e) {
model_->setNameFilterDisables(false);
ui_->list->setModel(model_);
// Create tree model
tree_model_ = new FileViewTreeModel(this);
tree_model_->SetNameFilters(filter_list_);
SetupTreeView();
ChangeFilePathWithoutUndo(QDir::homePath());
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
}
void FileView::SetupTreeView() {
// Use the new tree model with virtual roots
ui_->tree->setModel(tree_model_);
// Set the root paths in the model
tree_model_->SetRootPaths(tree_root_paths_);
// No need to set root index - the model handles virtual roots
}
void FileView::ToggleViewMode() {
tree_view_active_ = !tree_view_active_;
UpdateViewModeUI();
// Save the preference
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_view_active"_s, tree_view_active_);
s.endGroup();
}
void FileView::UpdateViewModeUI() {
if (tree_view_active_) {
ui_->view_stack->setCurrentWidget(ui_->tree_page);
// Hide navigation controls in tree view mode
ui_->back->setVisible(false);
ui_->forward->setVisible(false);
ui_->up->setVisible(false);
ui_->home->setVisible(false);
ui_->path->setVisible(false);
// Show tree root management buttons
ui_->add_tree_root->setVisible(true);
ui_->remove_tree_root->setVisible(true);
// Insert spacer in tree view if not already present
if (!view_mode_spacer_) {
view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_);
}
}
else {
ui_->view_stack->setCurrentWidget(ui_->list_page);
// Show navigation controls in list view mode
ui_->back->setVisible(true);
ui_->forward->setVisible(true);
ui_->up->setVisible(true);
ui_->home->setVisible(true);
ui_->path->setVisible(true);
// Hide tree root management buttons in list view
ui_->add_tree_root->setVisible(false);
ui_->remove_tree_root->setVisible(false);
// Remove spacer in list view
if (view_mode_spacer_) {
ui_->horizontalLayout->removeItem(view_mode_spacer_);
delete view_mode_spacer_;
view_mode_spacer_ = nullptr;
}
}
}
void FileView::AddTreeRootPath(const QString &path) {
if (!tree_root_paths_.contains(path)) {
tree_root_paths_.append(path);
SaveTreeRootPaths();
// Refresh the tree view to show the new root
if (tree_model_) {
SetupTreeView();
}
}
}
void FileView::RemoveTreeRootPath(const QString &path) {
tree_root_paths_.removeAll(path);
SaveTreeRootPaths();
// Refresh the tree view
if (tree_model_) {
SetupTreeView();
}
}
void FileView::SaveTreeRootPaths() {
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_root_paths"_s, tree_root_paths_);
s.endGroup();
}
void FileView::AddRootButtonClicked() {
const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
AddTreeRootPath(dir);
}
}
void FileView::RemoveRootButtonClicked() {
// Get currently selected item in tree
QModelIndex current = ui_->tree->currentIndex();
if (!current.isValid()) return;
QString path;
// Get the file path from the appropriate model
if (tree_model_) {
path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString();
}
if (path.isEmpty()) return;
const QString clean_path = QDir::cleanPath(path);
// Check if this path or any parent is a configured root
for (const QString &root : std::as_const(tree_root_paths_)) {
const QString clean_root = QDir::cleanPath(root);
if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) {
RemoveTreeRootPath(root);
return;
}
}
}
void FileView::keyPressEvent(QKeyEvent *e) {
switch (e->key()) {

View File

@@ -40,10 +40,12 @@ class QFileIconProvider;
class QUndoStack;
class QKeyEvent;
class QShowEvent;
class QSpacerItem;
class MusicStorage;
class TaskManager;
class Ui_FileView;
class FileViewTreeModel;
class FileView : public QWidget {
Q_OBJECT
@@ -76,12 +78,22 @@ class FileView : public QWidget {
void ChangeFilePath(const QString &new_path);
void ItemActivated(const QModelIndex &idx);
void ItemDoubleClick(const QModelIndex &idx);
void ToggleViewMode();
void Delete(const QStringList &filenames);
void DeleteFinished(const SongList &songs_with_errors);
public Q_SLOTS:
void AddTreeRootPath(const QString &path);
void RemoveTreeRootPath(const QString &path);
private:
void ChangeFilePathWithoutUndo(const QString &new_path);
void SetupTreeView();
void SaveTreeRootPaths();
void AddRootButtonClicked();
void RemoveRootButtonClicked();
void UpdateViewModeUI();
private:
class UndoCommand : public QUndoCommand {
@@ -110,16 +122,21 @@ class FileView : public QWidget {
Ui_FileView *ui_;
QFileSystemModel *model_;
FileViewTreeModel *tree_model_;
QUndoStack *undo_stack_;
SharedPtr<TaskManager> task_manager_;
SharedPtr<MusicStorage> storage_;
QString lazy_set_path_;
QStringList tree_root_paths_;
QStringList filter_list_;
ScopedPtr<QFileIconProvider> file_icon_provider_;
bool tree_view_active_;
QSpacerItem *view_mode_spacer_;
};
#endif // FILEVIEW_H

View File

@@ -95,8 +95,78 @@
<item>
<widget class="QLineEdit" name="path"/>
</item>
<item>
<widget class="QToolButton" name="add_tree_root">
<property name="toolTip">
<string>Add root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_tree_root">
<property name="toolTip">
<string>Remove selected root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toggle_view">
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Toggle between list and tree view</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="view_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="list_page">
<layout class="QVBoxLayout" name="list_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
@@ -121,12 +191,62 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="tree_page">
<layout class="QVBoxLayout" name="tree_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewTree" name="tree">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FileViewList</class>
<extends>QListView</extends>
<header>fileview/fileviewlist.h</header>
</customwidget>
<customwidget>
<class>FileViewTree</class>
<extends>QTreeView</extends>
<header>fileview/fileviewtree.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
// If just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
@@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use the current root path
// Otherwise, use the current root path
else {
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
if (path.length() > 20) {
@@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
case Qt::XButton2:
Q_EMIT Forward();
break;
// enqueue to playlist with middleClick
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QListView::mousePressEvent(e);
// we need to update the menu selection
// We need to update the menu selection
menu_selection_ = selectionModel()->selection();
MimeData *mimedata = new MimeData;

View File

@@ -0,0 +1,205 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <algorithm>
#include <utility>
#include <QWidget>
#include <QAbstractItemModel>
#include <QFileInfo>
#include <QDir>
#include <QMenu>
#include <QUrl>
#include <QCollator>
#include <QtEvents>
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "utilities/filemanagerutils.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
using namespace Qt::Literals::StringLiterals;
FileViewTree::FileViewTree(QWidget *parent)
: QTreeView(parent),
menu_(new QMenu(this)) {
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot);
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot);
menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot);
menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot);
menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot);
menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot);
menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser);
setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true);
setUniformRowHeights(true);
}
void FileViewTree::contextMenuEvent(QContextMenuEvent *e) {
menu_selection_ = selectionModel()->selection();
menu_->popup(e->globalPos());
e->accept();
}
QStringList FileViewTree::FilenamesFromSelection() const {
QStringList filenames;
const QModelIndexList indexes = menu_selection_.indexes();
FileViewTreeModel *tree_model = qobject_cast<FileViewTreeModel*>(model());
if (tree_model) {
for (const QModelIndex &index : indexes) {
if (index.column() == 0) {
QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString();
if (!path.isEmpty()) {
filenames << path;
}
}
}
}
QCollator collator;
collator.setNumericMode(true);
std::sort(filenames.begin(), filenames.end(), collator);
return filenames;
}
QList<QUrl> FileViewTree::UrlListFromSelection() const {
QList<QUrl> urls;
const QStringList filenames = FilenamesFromSelection();
urls.reserve(filenames.count());
for (const QString &filename : std::as_const(filenames)) {
urls << QUrl::fromLocalFile(filename);
}
return urls;
}
MimeData *FileViewTree::MimeDataFromSelection() const {
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
}
else {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use "Files" as default
else {
mimedata->name_for_new_playlist_ = tr("Files");
}
return mimedata;
}
void FileViewTree::LoadSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->clear_first_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::AddToPlaylistSlot() {
Q_EMIT AddToPlaylist(MimeDataFromSelection());
}
void FileViewTree::OpenInNewPlaylistSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->open_in_new_playlist_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::CopyToCollectionSlot() {
Q_EMIT CopyToCollection(UrlListFromSelection());
}
void FileViewTree::MoveToCollectionSlot() {
Q_EMIT MoveToCollection(UrlListFromSelection());
}
void FileViewTree::CopyToDeviceSlot() {
Q_EMIT CopyToDevice(UrlListFromSelection());
}
void FileViewTree::DeleteSlot() {
Q_EMIT Delete(FilenamesFromSelection());
}
void FileViewTree::EditTagsSlot() {
Q_EMIT EditTags(UrlListFromSelection());
}
void FileViewTree::mousePressEvent(QMouseEvent *e) {
switch (e->button()) {
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QTreeView::mousePressEvent(e);
// We need to update the menu selection
QItemSelectionModel *selection_model = selectionModel();
if (!selection_model) {
e->ignore();
return;
}
menu_selection_ = selection_model->selection();
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
mimedata->enqueue_now_ = true;
Q_EMIT AddToPlaylist(mimedata);
break;
}
default:
QTreeView::mousePressEvent(e);
break;
}
}
void FileViewTree::ShowInBrowser() {
Utilities::OpenInFileBrowser(UrlListFromSelection());
}

View File

@@ -0,0 +1,78 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREE_H
#define FILEVIEWTREE_H
#include <QObject>
#include <QTreeView>
#include <QList>
#include <QUrl>
#include <QString>
#include <QStringList>
class QWidget;
class QMimeData;
class QMenu;
class QMouseEvent;
class QContextMenuEvent;
class MimeData;
class FileViewTree : public QTreeView {
Q_OBJECT
public:
explicit FileViewTree(QWidget *parent = nullptr);
void mousePressEvent(QMouseEvent *e) override;
Q_SIGNALS:
void AddToPlaylist(QMimeData *data);
void CopyToCollection(const QList<QUrl> &urls);
void MoveToCollection(const QList<QUrl> &urls);
void CopyToDevice(const QList<QUrl> &urls);
void Delete(const QStringList &filenames);
void EditTags(const QList<QUrl> &urls);
protected:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
QStringList FilenamesFromSelection() const;
QList<QUrl> UrlListFromSelection() const;
MimeData *MimeDataFromSelection() const;
private Q_SLOTS:
void LoadSlot();
void AddToPlaylistSlot();
void OpenInNewPlaylistSlot();
void CopyToCollectionSlot();
void MoveToCollectionSlot();
void CopyToDeviceSlot();
void DeleteSlot();
void EditTagsSlot();
void ShowInBrowser();
private:
QMenu *menu_;
QItemSelection menu_selection_;
};
#endif // FILEVIEWTREE_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREEITEM_H
#define FILEVIEWTREEITEM_H
#include "config.h"
#include <QFileInfo>
#include "core/simpletreeitem.h"
class FileViewTreeItem : public SimpleTreeItem<FileViewTreeItem> {
public:
enum class Type {
Root, // Hidden root
VirtualRoot, // User-configured root paths
Directory, // File system directory
File // File system file
};
explicit FileViewTreeItem(SimpleTreeModel<FileViewTreeItem> *_model) : SimpleTreeItem<FileViewTreeItem>(_model), type(Type::Root), lazy_loaded(false) {}
explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem<FileViewTreeItem>(_parent), type(_type), lazy_loaded(false) {}
Type type;
QString file_path; // Absolute file system path
QFileInfo file_info; // Cached file info
bool lazy_loaded; // Whether children have been loaded
private:
Q_DISABLE_COPY(FileViewTreeItem)
};
Q_DECLARE_METATYPE(FileViewTreeItem::Type)
#endif // FILEVIEWTREEITEM_H

View File

@@ -0,0 +1,246 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QDir>
#include <QFileInfo>
#include <QFileIconProvider>
#include <QMimeData>
#include <QUrl>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "core/logging.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
using namespace Qt::Literals::StringLiterals;
FileViewTreeModel::FileViewTreeModel(QObject *parent)
: SimpleTreeModel<FileViewTreeItem>(new FileViewTreeItem(this), parent),
icon_provider_(new QFileIconProvider()) {
}
FileViewTreeModel::~FileViewTreeModel() {
delete root_;
delete icon_provider_;
}
Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const {
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return Qt::NoItemFlags;
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
case FileViewTreeItem::Type::File:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case FileViewTreeItem::Type::Root:
default:
return Qt::ItemIsEnabled;
}
}
QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const {
if (!idx.isValid()) return QVariant();
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return QVariant();
switch (role) {
case Qt::DisplayRole:
if (item->type == FileViewTreeItem::Type::VirtualRoot) {
return item->display_text.isEmpty() ? item->file_path : item->display_text;
}
return item->file_info.fileName();
case Qt::DecorationRole:
return GetIcon(item);
case Role_Type:
return QVariant::fromValue(item->type);
case Role_FilePath:
return item->file_path;
case Role_FileName:
return item->file_info.fileName();
default:
return QVariant();
}
}
bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Root and VirtualRoot always have children (or can have them)
if (item->type == FileViewTreeItem::Type::Root) return true;
if (item->type == FileViewTreeItem::Type::VirtualRoot) return true;
// Directories can have children
if (item->type == FileViewTreeItem::Type::Directory) {
return true;
}
// Files don't have children
return false;
}
bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Can fetch more if not yet lazy loaded
return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory);
}
void FileViewTreeModel::fetchMore(const QModelIndex &parent) {
FileViewTreeItem *item = IndexToItem(parent);
if (!item || item->lazy_loaded) return;
LazyLoad(item);
}
void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) {
if (item->lazy_loaded) return;
QDir dir(item->file_path);
if (!dir.exists()) {
item->lazy_loaded = true;
return;
}
// Apply name filters
const QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
if (!name_filters_.isEmpty()) {
dir.setNameFilters(name_filters_);
}
const QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst);
if (!entries.isEmpty()) {
BeginInsert(item, 0, static_cast<int>(entries.count()) - 1);
for (const QFileInfo &entry : entries) {
FileViewTreeItem *child = new FileViewTreeItem(
entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File,
item
);
child->file_path = entry.absoluteFilePath();
child->file_info = entry;
child->lazy_loaded = false;
child->display_text = entry.fileName();
}
EndInsert();
}
item->lazy_loaded = true;
}
QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const {
if (!item) return QIcon();
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
return icon_provider_->icon(QFileIconProvider::Folder);
case FileViewTreeItem::Type::File:
return icon_provider_->icon(item->file_info);
default:
return QIcon();
}
}
QStringList FileViewTreeModel::mimeTypes() const {
return QStringList() << u"text/uri-list"_s;
}
QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
const FileViewTreeItem *item = IndexToItem(idx);
if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) {
urls << QUrl::fromLocalFile(item->file_path);
}
}
if (urls.isEmpty()) return nullptr;
QMimeData *data = new QMimeData();
data->setUrls(urls);
return data;
}
void FileViewTreeModel::SetRootPaths(const QStringList &paths) {
Reset();
for (const QString &path : paths) {
QFileInfo info(path);
if (!info.exists() || !info.isDir()) continue;
FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_);
virtual_root->file_path = info.absoluteFilePath();
virtual_root->file_info = info;
virtual_root->display_text = info.absoluteFilePath();
virtual_root->lazy_loaded = false;
}
}
void FileViewTreeModel::SetNameFilters(const QStringList &filters) {
name_filters_ = filters;
}
void FileViewTreeModel::Reset() {
beginResetModel();
// Clear children without notifications since we're in a reset
qDeleteAll(root_->children);
root_->children.clear();
endResetModel();
}

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILEVIEWTREEMODEL_H
#define FILEVIEWTREEMODEL_H
#include "config.h"
#include <QObject>
#include <QVariant>
#include <QStringList>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "fileviewtreeitem.h"
class QFileIconProvider;
class QMimeData;
class FileViewTreeModel : public SimpleTreeModel<FileViewTreeItem> {
Q_OBJECT
public:
explicit FileViewTreeModel(QObject *parent = nullptr);
~FileViewTreeModel() override;
enum Role {
Role_Type = Qt::UserRole + 1,
Role_FilePath,
Role_FileName,
RoleCount
};
// QAbstractItemModel
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QVariant data(const QModelIndex &idx, const int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
void SetRootPaths(const QStringList &paths);
void SetNameFilters(const QStringList &filters);
private:
void Reset();
void LazyLoad(FileViewTreeItem *item);
QIcon GetIcon(const FileViewTreeItem *item) const;
private:
QFileIconProvider *icon_provider_;
QStringList name_filters_;
};
#endif // FILEVIEWTREEMODEL_H

View File

@@ -1,159 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QAbstractItemModel>
#include <QString>
#include <QStringList>
#include <QFileInfo>
#include <QDir>
#include <QDirIterator>
#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<int>(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);
Q_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;
}
Q_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;
}
}
}

View File

@@ -1,77 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef PROJECTMPRESETMODEL_H
#define PROJECTMPRESETMODEL_H
#include "config.h"
#include <QList>
#include <QString>
#include <QAbstractItemModel>
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 Q_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<Preset> all_presets_;
};
#endif // PROJECTMPRESETMODEL_H

View File

@@ -1,469 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <string>
#include <memory>
#ifdef HAVE_PROJECTM4
# include <projectM-4/types.h>
# include <projectM-4/core.h>
# include <projectM-4/parameters.h>
# include <projectM-4/memory.h>
# include <projectM-4/audio.h>
# include <projectM-4/render_opengl.h>
# include <projectM-4/playlist_types.h>
# include <projectM-4/playlist_core.h>
# include <projectM-4/playlist_memory.h>
# include <projectM-4/playlist_items.h>
# include <projectM-4/playlist_playback.h>
#else
# include <libprojectM/projectM.hpp>
#endif // HAVE_PROJECTM4
#include <QCoreApplication>
#include <QGraphicsScene>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFileInfo>
#include <QScopeGuard>
#include <QPainter>
#include <QRandomGenerator>
#include <QMessageBox>
#include <QTimerEvent>
#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<projectM>(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<size_t>(width * pixel_ratio), static_cast<size_t>(height * pixel_ratio));
}
#else
if (projectm_) {
projectm_->projectM_resetGL(static_cast<int>(width * pixel_ratio), static_cast<int>(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, const int channels) {
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)) / channels;
const int16_t *data = reinterpret_cast<int16_t*>(map.data);
projectm_pcm_add_int16(projectm_instance_, data, samples_per_channel, static_cast<projectm_channels>(channels));
}
#else
if (projectm_) {
const int16_t samples_per_channel = static_cast<int16_t>((map.size / sizeof(int16_t)) / channels);
const int16_t *data = reinterpret_cast<int16_t*>(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<int>(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<int>(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<int>(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();
}

View File

@@ -1,114 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef PROJECTMVISUALIZATION_H
#define PROJECTMVISUALIZATION_H
#include "config.h"
#include <memory>
#include <vector>
#ifdef HAVE_PROJECTM4
# include <projectM-4/types.h>
# include <projectM-4/playlist_types.h>
#else
# include <libprojectM/projectM.hpp>
#endif
#include <QString>
#include <QStringList>
#include <QGraphicsScene>
#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, const int channels) override;
public Q_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 Q_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> projectm_;
#endif
Mode mode_;
int duration_;
std::vector<int> default_rating_list_;
int texture_size_;
QString preset_path_;
};
#endif // PROJECTMVISUALIZATION_H

View File

@@ -1,328 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QOpenGLWidget>
#include <QGraphicsProxyWidget>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QShortcut>
#include <QActionGroup>
#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_);
setViewport(new VisualizationOpenGLWidget(projectm_visualization_));
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();
}
}
void VisualizationContainer::enterEvent(QEnterEvent *e) {
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_);
}

View File

@@ -1,103 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONCONTAINER_H
#define VISUALIZATIONCONTAINER_H
#include "config.h"
#include <QBasicTimer>
#include <QGraphicsView>
#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;
class QEnterEvent;
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 Q_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;
void enterEvent(QEnterEvent *e) override;
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 Q_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

View File

@@ -1,39 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#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();
glEnable(GL_BLEND);
}

View File

@@ -1,42 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONOPENGLWIDGET_H
#define VISUALIZATIONOPENGLWIDGET_H
#include "config.h"
#include <QOpenGLWidget>
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

View File

@@ -1,116 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QString>
#include <QBasicTimer>
#include <QTimeLine>
#include <QGraphicsProxyWidget>
#include <QTimerEvent>
#include <QMouseEvent>
#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() {
Q_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);
}

View File

@@ -1,71 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONOVERLAY_H
#define VISUALIZATIONOVERLAY_H
#include "config.h"
#include <QWidget>
#include <QString>
#include <QBasicTimer>
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 Q_SLOTS:
void SetVisible(const bool visible);
Q_SIGNALS:
void OpacityChanged(const qreal value);
void ShowPopupMenu(const QPoint &pos);
protected:
// QWidget
void timerEvent(QTimerEvent *e);
private Q_SLOTS:
void ShowSettingsMenu();
private:
Ui_VisualizationOverlay *ui_;
QTimeLine *fade_timeline_;
QBasicTimer fade_out_timeout_;
bool visible_;
};
#endif // VISUALIZATIONOVERLAY_H

View File

@@ -1,234 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>VisualizationOverlay</class>
<widget class="QWidget" name="VisualizationOverlay">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>523</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">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;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>210</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="song_title">
<property name="text">
<string>Strawberry</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="previous">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="play_pause">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="stop">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="next">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>13</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="toolTip">
<string>Visualizations Settings</string>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,88 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QPushButton>
#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<int>::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<int>::of(&QSpinBox::valueChanged), projectm_visualization_, &ProjectMVisualization::SetDuration);
ui_->mode->setCurrentIndex(static_cast<int>(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<ProjectMVisualization::Mode>(mode));
}
void VisualizationSelector::SelectAll() { projectm_visualization_->preset_model()->SelectAll(); }
void VisualizationSelector::SelectNone() { projectm_visualization_->preset_model()->SelectNone(); }

View File

@@ -1,61 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONSELECTOR_H
#define VISUALIZATIONSELECTOR_H
#include "config.h"
#include <QDialog>
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 Q_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

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>VisualizationSelector</class>
<widget class="QDialog" name="VisualizationSelector">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>475</height>
</rect>
</property>
<property name="windowTitle">
<string>Select visualizations</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Visualization mode</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="mode">
<item>
<property name="text">
<string>Random visualization</string>
</property>
</item>
<item>
<property name="text">
<string>Choose from the list</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Delay between visualizations</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="delay">
<property name="suffix">
<string> seconds</string>
</property>
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="value">
<number>15</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="list">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>mode</tabstop>
<tabstop>delay</tabstop>
<tabstop>list</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>VisualizationSelector</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>VisualizationSelector</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>