From 91c6672f5caa04a379238f97e54667a318d05370 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Mon, 14 Jul 2025 21:26:53 +0800 Subject: [PATCH] UI(Windows): add task icon progress and buttons --- CMakeLists.txt | 4 + mainwindow.cpp | 31 +++- mainwindow.h | 2 + taskbarmanager.cpp | 383 +++++++++++++++++++++++++++++++++++++++ taskbarmanager.h | 151 +++++++++++++++ taskbarmanager_dummy.cpp | 97 ++++++++++ 6 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 taskbarmanager.cpp create mode 100644 taskbarmanager.h create mode 100644 taskbarmanager_dummy.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c5de58..8d3a29e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ set (PMUSIC_HEADER_FILES lyricsmanager.h fftspectrum.h playbackprogressindicator.h + taskbarmanager.h ) set (PMUSIC_UI_FILES @@ -84,6 +85,9 @@ TS_FILES if (WIN32) target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc) + target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp) +else () + target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp) endif () if (NOT TagLib_FOUND) diff --git a/mainwindow.cpp b/mainwindow.cpp index 692ddc4..73f1775 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -8,6 +8,7 @@ #include "playlistmanager.h" #include "fftspectrum.h" #include "lrcbar.h" +#include "taskbarmanager.h" // taglib #ifndef NO_TAGLIB @@ -34,6 +35,7 @@ #include #include #include +#include constexpr QSize miniSize(490, 160); constexpr QSize fullSize(490, 420); @@ -47,6 +49,7 @@ MainWindow::MainWindow(QWidget *parent) , m_fftSpectrum(new FFTSpectrum(this)) , m_lrcbar(new LrcBar(nullptr)) , m_playlistManager(new PlaylistManager(this)) + , m_taskbarManager(new TaskBarManager(this)) { ui->setupUi(this); m_playlistManager->setAutoLoadFilterSuffixes({ @@ -70,12 +73,21 @@ MainWindow::MainWindow(QWidget *parent) setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint); setAttribute(Qt::WA_TranslucentBackground, true); + m_taskbarManager->setCanTogglePlayback(true); + m_taskbarManager->setCanSkipBackward(true); + m_taskbarManager->setCanSkipForward(true); + m_taskbarManager->setShowProgress(true); + loadConfig(); loadSkinData(); initConnections(); initUiAndAnimation(); centerWindow(); + + QTimer::singleShot(1000, [this](){ + m_taskbarManager->setWinId(window()->winId()); + }); } MainWindow::~MainWindow() @@ -492,6 +504,7 @@ void MainWindow::initConnections() ui->nowTimeLabel->setText(ms2str(pos)); if (m_mediaPlayer->duration() != 0) { ui->playbackProgressIndicator->setPosition(pos); + m_taskbarManager->setProgressValue(pos); } m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration()); }); @@ -506,6 +519,7 @@ void MainWindow::initConnections() connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, [=](qint64 dua) { ui->playbackProgressIndicator->setDuration(dua); + m_taskbarManager->setProgressMaximum(dua); ui->totalTimeLabel->setText(ms2str(dua)); }); @@ -519,6 +533,7 @@ void MainWindow::initConnections() ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png")); break; } + m_taskbarManager->setPlaybackState(newState); }); connect(m_audioOutput, &QAudioOutput::volumeChanged, this, [=](float vol) { @@ -559,9 +574,21 @@ void MainWindow::initConnections() } }); - connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) { + connect(m_taskbarManager, &TaskBarManager::togglePlayback, this, [this](){ + on_playBtn_clicked(); + }); + + connect(m_taskbarManager, &TaskBarManager::skipBackward, this, [this](){ + on_prevBtn_clicked(); + }); + + connect(m_taskbarManager, &TaskBarManager::skipForward, this, [this](){ + on_nextBtn_clicked(); + }); + + connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) { qDebug() << error << errorString; - }); + }); } void MainWindow::loadConfig() diff --git a/mainwindow.h b/mainwindow.h index cbb941f..1907c76 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -22,6 +22,7 @@ QT_END_NAMESPACE class FFTSpectrum; class LrcBar; class PlaylistManager; +class TaskBarManager; class MainWindow : public QMainWindow { Q_OBJECT @@ -103,6 +104,7 @@ private: LrcBar *m_lrcbar; QPropertyAnimation *m_fadeOutAnimation; PlaylistManager *m_playlistManager; + TaskBarManager *m_taskbarManager; void initUiAndAnimation(); void initConnections(); diff --git a/taskbarmanager.cpp b/taskbarmanager.cpp new file mode 100644 index 0000000..faadc87 --- /dev/null +++ b/taskbarmanager.cpp @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: 2024 (c) Jack Hill +// SPDX-FileCopyrightText: 2025 (c) Gary Wang +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This file is modified based on Elisa's taskmanager.cpp implementation, +// with its Qt Quick usage removed. + +#include "taskbarmanager.h" + +#include +#include +#include + +#include +#include +#include + +constexpr int numOfButtons = 3; + +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +using namespace Qt::Literals::StringLiterals; + +static HICON hiconFromTheme(const QString &iconName) +#else +static HICON hiconFromTheme(const QIcon::ThemeIcon iconName) +#endif +{ + const auto width = GetSystemMetrics(SM_CXSMICON); + const auto height = GetSystemMetrics(SM_CYSMICON); + return QIcon::fromTheme(iconName).pixmap(width, height).toImage().toHICON(); +} + +class TaskBarManagerPrivate +{ +public: + + ITaskbarList3 *taskBar = nullptr; + HWND hwnd = nullptr; + + /** + * Whether the toolbar has been added to the taskbar + */ + bool addedThumbButtons = false; + + QMediaPlayer::PlaybackState playbackState = QMediaPlayer::StoppedState; + + bool showProgress = false; + qlonglong progressMaximum = 0; + qlonglong progressValue = 0; + +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) + HICON skipBackwardIcon = hiconFromTheme(u"media-skip-backward"_s); + HICON skipForwardIcon = hiconFromTheme(u"media-skip-forward"_s); + HICON playIcon = hiconFromTheme(u"media-playback-start"_s); + HICON pauseIcon = hiconFromTheme(u"media-playback-pause"_s); +#else + HICON skipBackwardIcon = hiconFromTheme(QIcon::ThemeIcon::MediaSkipBackward); + HICON skipForwardIcon = hiconFromTheme(QIcon::ThemeIcon::MediaSkipForward); + HICON playIcon = hiconFromTheme(QIcon::ThemeIcon::MediaPlaybackStart); + HICON pauseIcon = hiconFromTheme(QIcon::ThemeIcon::MediaPlaybackPause); +#endif + + /** + * Contains the current state of each button + */ + struct ButtonData { + HICON icon = nullptr; + bool enabled = false; + }; + std::array buttonData = {{ + {skipBackwardIcon, false}, + {playIcon, false}, + {skipForwardIcon, false} + }}; + + /** + * For convenience when reading code + */ + enum ButtonID { + SkipBackward = 0, + TogglePlayback = 1, + SkipForward = 2, + }; + + TaskBarManagerPrivate() + { + HRESULT result = CoCreateInstance(CLSID_TaskbarList, + nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&taskBar)); + if (FAILED(result)) { + taskBar = nullptr; + qWarning() << "Failed to create Windows taskbar instance"; + return; + } + } + + ~TaskBarManagerPrivate() + { + if (taskBar) { + taskBar->Release(); + } + DestroyIcon(skipForwardIcon); + DestroyIcon(skipBackwardIcon); + DestroyIcon(pauseIcon); + DestroyIcon(playIcon); + } + + /** + * Update the native window handle for the taskbar instance + */ + void setHWND(HWND newHwnd) + { + if (hwnd == newHwnd) { + return; + } + + hwnd = newHwnd; + addedThumbButtons = false; + if (hwnd) { + updateProgressFlags(); + updateProgressValue(); + setupTaskBarManagerButtons(); + } + } + + /** + * Update the taskbar progress flags + */ + void updateProgressFlags() + { + if (taskBar && hwnd) { + taskBar->SetProgressState(hwnd, progressFlags()); + } + } + + /** + * Update the taskbar progress value, only if the taskbar is showing progress + */ + void updateProgressValue() + { + if (taskBar && hwnd && showProgress && playbackState != QMediaPlayer::StoppedState) { + taskBar->SetProgressValue(hwnd, progressValue, progressMaximum); + } + } + + /** + * Update the state of a single toolbutton + */ + void updateButton(const ButtonID buttonId) + { + if (!taskBar || !hwnd || !addedThumbButtons) { + return; + } + THUMBBUTTON thumbButtons[1]; + initThumbButton(thumbButtons, buttonId); + taskBar->ThumbBarUpdateButtons(hwnd, 1, thumbButtons); + } + +private: + /** + * Convert playbackState into windows taskbar flags + */ + TBPFLAG progressFlags() const + { + if (!showProgress) { + return TBPF_NOPROGRESS; + } + + switch (playbackState) { + case QMediaPlayer::StoppedState: + return TBPF_NOPROGRESS; + case QMediaPlayer::PlayingState: + return TBPF_NORMAL; + case QMediaPlayer::PausedState: + return TBPF_PAUSED; + default: + return TBPF_NOPROGRESS; + } + } + + /** + * Initialize a single toolbutton + */ + void initThumbButton(THUMBBUTTON *button, const ButtonID id) + { + button->dwMask = THB_ICON | THB_FLAGS; + button->iId = id; + button->hIcon = buttonData.at(id).icon; + button->dwFlags = buttonData.at(id).enabled ? THBF_ENABLED : THBF_DISABLED; + } + + /** + * Initialize the toolbar + */ + void setupTaskBarManagerButtons() + { + if (!taskBar || !hwnd || addedThumbButtons) { + return; + } + + THUMBBUTTON thumbButtons[numOfButtons]; + THUMBBUTTON *currButton = thumbButtons; + for (int i = 0; i < numOfButtons; ++i) { + initThumbButton(currButton++, static_cast(i)); + } + + // win32 api reference says we should pass &thumbButtons but this doesn't compile + HRESULT result = taskBar->ThumbBarAddButtons(hwnd, numOfButtons, thumbButtons); + if (FAILED(result)) { + qWarning() << "Failed to create Windows taskbar buttons"; + } else { + addedThumbButtons = true; + } + } +}; + +TaskBarManager::TaskBarManager(QObject *parent) + : QObject(parent) + , d(std::make_unique()) +{ + QAbstractEventDispatcher::instance()->installNativeEventFilter(this); +} + +TaskBarManager::~TaskBarManager() = default; + +bool TaskBarManager::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) +{ + if (eventType == "windows_generic_MSG") { + MSG *msg = static_cast(message); + if (msg->hwnd != d->hwnd || msg->message != WM_COMMAND || HIWORD(msg->wParam) != THBN_CLICKED) { + return false; + } + + switch (LOWORD(msg->wParam)) { + case TaskBarManagerPrivate::SkipBackward: + Q_EMIT skipBackward(); + return true; + case TaskBarManagerPrivate::TogglePlayback: + Q_EMIT togglePlayback(); + return true; + case TaskBarManagerPrivate::SkipForward: + Q_EMIT skipForward(); + return true; + } + } + return false; +} + +QMediaPlayer::PlaybackState TaskBarManager::playbackState() const +{ + return d->playbackState; +} + +bool TaskBarManager::showProgress() const +{ + return d->showProgress; +} + +qulonglong TaskBarManager::progressMaximum() const +{ + return d->progressMaximum; +} + +qulonglong TaskBarManager::progressValue() const +{ + return d->progressValue; +} + +bool TaskBarManager::canSkipBackward() const +{ + return d->buttonData.at(TaskBarManagerPrivate::SkipBackward).enabled; +} + +bool TaskBarManager::canSkipForward() const +{ + return d->buttonData.at(TaskBarManagerPrivate::SkipForward).enabled; +} + +bool TaskBarManager::canTogglePlayback() const +{ + return d->buttonData.at(TaskBarManagerPrivate::TogglePlayback).enabled; +} + +void TaskBarManager::setWinId(WId winId) +{ + d->setHWND(reinterpret_cast(winId)); +} + +void TaskBarManager::setPlaybackState(const QMediaPlayer::PlaybackState newPlaybackState) +{ + if (d->playbackState == newPlaybackState) { + return; + } + + d->playbackState = newPlaybackState; + Q_EMIT playbackStateChanged(); + + d->buttonData.at(TaskBarManagerPrivate::TogglePlayback).icon = + d->playbackState == QMediaPlayer::PlayingState ? d->pauseIcon : d->playIcon; + d->updateButton(TaskBarManagerPrivate::TogglePlayback); + d->updateProgressFlags(); +} + +void TaskBarManager::setShowProgress(const bool showProgress) +{ + if (d->showProgress == showProgress) { + return; + } + + d->showProgress = showProgress; + Q_EMIT showProgressChanged(); + + d->updateProgressFlags(); +} + +void TaskBarManager::setProgressMaximum(const qlonglong newMaximum) +{ + if (d->progressMaximum == newMaximum) { + return; + } + + d->progressMaximum = newMaximum; + Q_EMIT progressMaximumChanged(); + + if (d->progressValue > d->progressMaximum) { + d->progressValue = d->progressMaximum; + Q_EMIT progressValueChanged(); + } + + d->updateProgressValue(); +} + +void TaskBarManager::setProgressValue(const qlonglong newValue) +{ + if (d->progressValue == newValue) { + return; + } + + d->progressValue = newValue < d->progressMaximum ? newValue : d->progressMaximum; + Q_EMIT progressValueChanged(); + + d->updateProgressValue(); +} + +void TaskBarManager::setCanSkipBackward(const bool canSkip) +{ + if (d->buttonData[TaskBarManagerPrivate::SkipBackward].enabled == canSkip) { + return; + } + + d->buttonData[TaskBarManagerPrivate::SkipBackward].enabled = canSkip; + Q_EMIT canSkipBackwardChanged(); + + d->updateButton(TaskBarManagerPrivate::SkipBackward); +} + +void TaskBarManager::setCanSkipForward(const bool canSkip) +{ + if (d->buttonData[TaskBarManagerPrivate::SkipForward].enabled == canSkip) { + return; + } + + d->buttonData[TaskBarManagerPrivate::SkipForward].enabled = canSkip; + Q_EMIT canSkipForwardChanged(); + + d->updateButton(TaskBarManagerPrivate::SkipForward); +} + +void TaskBarManager::setCanTogglePlayback(const bool canToggle) +{ + if (d->buttonData[TaskBarManagerPrivate::TogglePlayback].enabled == canToggle) { + return; + } + + d->buttonData[TaskBarManagerPrivate::TogglePlayback].enabled = canToggle; + Q_EMIT canTogglePlaybackChanged(); + + d->updateButton(TaskBarManagerPrivate::TogglePlayback); +} + +#include "moc_taskbarmanager.cpp" diff --git a/taskbarmanager.h b/taskbarmanager.h new file mode 100644 index 0000000..736efd8 --- /dev/null +++ b/taskbarmanager.h @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2024 (c) Jack Hill +// SPDX-FileCopyrightText: 2025 (c) Gary Wang +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This file is modified based on Elisa's taskmanager.cpp implementation, +// with its Qt Quick usage removed. + +#ifndef TASKBAR_H +#define TASKBAR_H + +#include +#include +#include + +#include + +class TaskBarManagerPrivate; + +/** + * Windows taskbar + */ +class TaskBarManager : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT + + /** + * Whether the media is playing, paused, or stopped + */ + Q_PROPERTY(QMediaPlayer::PlaybackState playbackState + READ playbackState + WRITE setPlaybackState + NOTIFY playbackStateChanged) + + /** + * Whether to show track progress on the taskbar + */ + Q_PROPERTY(bool showProgress + READ showProgress + WRITE setShowProgress + NOTIFY showProgressChanged) + + /** + * Maximum possible progress + */ + Q_PROPERTY(qulonglong progressMaximum + READ progressMaximum + WRITE setProgressMaximum + NOTIFY progressMaximumChanged) + + /** + * Current progress; this is always clamped to be in the range [0, progressMaximum] + */ + Q_PROPERTY(qulonglong progressValue + READ progressValue + WRITE setProgressValue + NOTIFY progressValueChanged) + + /** + * Whether the "Skip Backward" button is enabled + */ + Q_PROPERTY(bool canSkipBackward + READ canSkipBackward + WRITE setCanSkipBackward + NOTIFY canSkipBackwardChanged) + + /** + * Whether the "Skip Forward" button is enabled + */ + Q_PROPERTY(bool canSkipForward + READ canSkipForward + WRITE setCanSkipForward + NOTIFY canSkipForwardChanged) + + /** + * Whether the "Toggle Playback" button is enabled + */ + Q_PROPERTY(bool canTogglePlayback + READ canTogglePlayback + WRITE setCanTogglePlayback + NOTIFY canTogglePlaybackChanged) + +public: + + explicit TaskBarManager(QObject *parent = nullptr); + + ~TaskBarManager() override; + + bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override; + + [[nodiscard]] QMediaPlayer::PlaybackState playbackState() const; + + [[nodiscard]] bool showProgress() const; + + [[nodiscard]] qulonglong progressMaximum() const; + + [[nodiscard]] qulonglong progressValue() const; + + [[nodiscard]] bool canSkipBackward() const; + + [[nodiscard]] bool canSkipForward() const; + + [[nodiscard]] bool canTogglePlayback() const; + +Q_SIGNALS: + + void playbackStateChanged(); + + void showProgressChanged(); + + void progressMaximumChanged(); + + void progressValueChanged(); + + void canSkipBackwardChanged(); + + void canSkipForwardChanged(); + + void canTogglePlaybackChanged(); + + void skipBackward(); + + void skipForward(); + + void togglePlayback(); + +public Q_SLOTS: + + void setWinId(WId win); + + void setPlaybackState(QMediaPlayer::PlaybackState newPlaybackState); + + void setShowProgress(bool showProgress); + + void setProgressMaximum(qlonglong newMaximum); + + void setProgressValue(qlonglong newValue); + + void setCanSkipBackward(bool canSkip); + + void setCanSkipForward(bool canSkip); + + void setCanTogglePlayback(bool canToggle); + +private: + + std::unique_ptr d; + +}; + +#endif // TASKBAR_H diff --git a/taskbarmanager_dummy.cpp b/taskbarmanager_dummy.cpp new file mode 100644 index 0000000..35c05f7 --- /dev/null +++ b/taskbarmanager_dummy.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "taskbarmanager.h" + +#include + +class TaskBarManagerPrivate {}; + +TaskBarManager::TaskBarManager(QObject *parent) + : QObject(parent) +{ +} + +TaskBarManager::~TaskBarManager() = default; + +bool TaskBarManager::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *) +{ + return false; +} + +QMediaPlayer::PlaybackState TaskBarManager::playbackState() const +{ + return QMediaPlayer::StoppedState; +} + +bool TaskBarManager::showProgress() const +{ + return false; +} + +qulonglong TaskBarManager::progressMaximum() const +{ + return 100; +} + +qulonglong TaskBarManager::progressValue() const +{ + return 50; +} + +bool TaskBarManager::canSkipBackward() const +{ + return false; +} + +bool TaskBarManager::canSkipForward() const +{ + return false; +} + +bool TaskBarManager::canTogglePlayback() const +{ + return false; +} + +void TaskBarManager::setWinId(WId winId) +{ +} + +void TaskBarManager::setPlaybackState(const QMediaPlayer::PlaybackState newPlaybackState) +{ + Q_UNUSED(newPlaybackState); +} + +void TaskBarManager::setShowProgress(const bool showProgress) +{ + Q_UNUSED(showProgress); +} + +void TaskBarManager::setProgressMaximum(const qlonglong newMaximum) +{ + Q_UNUSED(newMaximum); +} + +void TaskBarManager::setProgressValue(const qlonglong newValue) +{ + Q_UNUSED(newValue); +} + +void TaskBarManager::setCanSkipBackward(const bool canSkip) +{ + Q_UNUSED(canSkip); +} + +void TaskBarManager::setCanSkipForward(const bool canSkip) +{ + Q_UNUSED(canSkip); +} + +void TaskBarManager::setCanTogglePlayback(const bool canToggle) +{ + Q_UNUSED(canToggle); +} + +#include "moc_taskbarmanager.cpp"