1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
|
/*
SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "multiplexermodel.h"
#include <KLocalizedString>
#include "mpris2sourcemodel.h"
#include "multiplexer.h"
#include "multiplexermodel.h"
#include "playercontainer.h"
std::shared_ptr<MultiplexerModel> MultiplexerModel::self()
{
static std::weak_ptr<MultiplexerModel> s_model;
if (s_model.expired()) {
auto ptr = std::make_shared<MultiplexerModel>();
s_model = ptr;
return ptr;
}
return s_model.lock();
}
MultiplexerModel::MultiplexerModel(QObject *parent)
: QAbstractListModel(parent)
, m_multiplexer(Multiplexer::self())
{
updateActivePlayer();
/*
# Why is Qt::QueuedConnection used?
If there are only one player remaining and the last player is closed:
1. Mpris2SourceModel::rowsRemoved -> Mpris2FilterProxyModel::rowsRemoved
3. Mpris2FilterProxyModel::rowsRemoved -> Multiplexer::onRowsRemoved
4. In Multiplexer::onRowsRemoved, activePlayerIndex is updated -> Multiplexer::activePlayerIndexChanged
5. Multiplexer::activePlayerIndexChanged -> **Qt::QueuedConnection**, so MultiplexerModel::updateActivePlayer() is not called in this context。
⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
Can't call updateActivePlayer() in the current context because updateActivePlayer() will emit rowsRemoved, which confuses Mpris2Model
because at this moment Mpris2FilterProxyModel::rowsRemoved is not finished!!! Without Qt::QueuedConnection there will be a CRASH!
💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣💣
6. After Mpris2SourceModel::rowsRemoved, the container is deleted -> PlayerContainer::destroyed
7. PlayerContainer::destroyed -> MultiplexerModel::updateActivePlayer
8. MultiplexerModel::updateActivePlayer -> (no player now) MultiplexerModel::rowsRemoved
9. The queued connection now calls MultiplexerModel::updateActivePlayer(), but there is no player now so it just simply returns.
# Why is this connection needed?
If there is more than one player, and the active player changes to another player:
1. activePlayerIndex is updated -> Multiplexer::activePlayerIndexChanged
2. Multiplexer::activePlayerIndexChanged -> **Qt::QueuedConnection**, so MultiplexerModel::updateActivePlayer() is not called in this context
3. The queued connection now calls MultiplexerModel::updateActivePlayer(), and (m_multiplexer->activePlayer().value() != m_activePlayer) is satisfied,
so a new player becomes the starred player.
*/
connect(m_multiplexer.get(), &Multiplexer::activePlayerIndexChanged, this, &MultiplexerModel::updateActivePlayer, Qt::QueuedConnection);
}
MultiplexerModel::~MultiplexerModel()
{
// MultiplexerModel is destroyed before PlayerContainer but it's not guaranteed the destroyed signal is disconnected after ~MultiplexerModel()
if (m_activePlayer) {
m_activePlayer->disconnect(this);
}
}
QVariant MultiplexerModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid) || !m_activePlayer) {
return {};
}
switch (role) {
case Mpris2SourceModel::IconNameRole:
return QStringLiteral("emblem-favorite");
case Mpris2SourceModel::IsMultiplexerRole:
return true;
default:
return Mpris2SourceModel::dataFromPlayer(m_activePlayer, role);
}
}
int MultiplexerModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return m_activePlayer ? 1 : 0;
}
QHash<int, QByteArray> MultiplexerModel::roleNames() const
{
return Mpris2SourceModel::self()->roleNames();
}
void MultiplexerModel::updateActivePlayer()
{
if (m_activePlayer && !m_multiplexer->activePlayer().value()) {
beginRemoveRows({}, 0, 0);
disconnect(m_activePlayer, nullptr, this, nullptr);
m_activePlayer = nullptr;
endRemoveRows();
} else if (m_multiplexer->activePlayer().value() != m_activePlayer) {
if (!m_activePlayer) {
beginInsertRows({}, 0, 0);
m_activePlayer = m_multiplexer->activePlayer().value();
endInsertRows();
} else {
disconnect(m_activePlayer, nullptr, this, nullptr);
m_activePlayer = m_multiplexer->activePlayer().value();
Q_EMIT dataChanged(index(0, 0), index(0, 0));
}
connect(m_activePlayer, &QObject::destroyed, this, &MultiplexerModel::updateActivePlayer);
// Property bindings
connect(m_activePlayer, &AbstractPlayerContainer::canControlChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanControlRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::trackChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::TrackRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canGoNextChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanGoNextRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canGoPreviousChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanGoPreviousRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canPlayChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanPlayRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canPauseChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanPauseRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canStopChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanStopRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canSeekChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanSeekRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::loopStatusChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::LoopStatusRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::playbackStatusChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::PlaybackStatusRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::positionChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::PositionRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::rateChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::RateRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::shuffleChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::ShuffleRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::volumeChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::VolumeRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::artUrlChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::ArtUrlRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::artistChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::ArtistRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::albumChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::AlbumRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::lengthChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::LengthRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canQuitChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanQuitRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canRaiseChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanRaiseRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::canSetFullscreenChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::CanSetFullscreenRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::desktopEntryChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::DesktopEntryRole, Mpris2SourceModel::IconNameRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::identityChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::IdentityRole});
});
connect(m_activePlayer, &AbstractPlayerContainer::kdePidChanged, this, [this] {
Q_EMIT dataChanged(index(0, 0), index(0, 0), {Mpris2SourceModel::KDEPidRole});
});
}
}
#include "moc_multiplexermodel.cpp"
|