Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/plugin-sound/operation/soundworker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ SoundWorker::SoundWorker(SoundModel *model, QObject *parent)

void SoundWorker::initConnect()
{
auto refreshCards = [this] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting the new lambdas and headphone-detection logic into small named helper methods/slots so the connection setup and cardsChanged() body remain simple and declarative.

You can reduce the added complexity substantially by extracting the inline lambdas into small, named member functions/slots and helpers, while keeping behavior identical.

1. initConnect() lambdas

initConnect() is now mixing connection wiring with non‑trivial logic in lambdas (refreshCards, PortEnabledChanged, QMediaDevices connections).

Suggestion: extract refreshCards and PortEnabledChanged logic into private member functions/slots.

// soundworker.h
private slots:
    void refreshCardsSnapshot();
    void onPortEnabledChanged(uint cardId, const QString &portId, bool enabled);
// soundworker.cpp
void SoundWorker::refreshCardsSnapshot()
{
    if (!m_soundDBusInter)
        return;

    m_model->setAudioCards(m_soundDBusInter->cardsWithoutUnavailable());
}

void SoundWorker::onPortEnabledChanged(uint cardId, const QString &portId, bool enabled)
{
    Port *port = m_model->findPort(portId, cardId);
    if (!port) {
        refreshCardsSnapshot();
        return;
    }

    port->setEnabled(enabled);
    m_model->updatePortCombo();
    m_model->updateSoundDeviceModel(port);
    m_model->updateActiveComboIndex();
}

Then initConnect() becomes declarative again:

void SoundWorker::initConnect()
{
    // ...
    connect(m_soundDBusInter, &SoundDBusProxy::CardsWithoutUnavailableChanged,
            m_model, &SoundModel::setAudioCards);
    connect(m_soundDBusInter, &SoundDBusProxy::CardsChanged,
            m_model, &SoundModel::setAudioCards);

    connect(m_soundDBusInter, &SoundDBusProxy::SinksChanged,
            this, [this](const QList<QDBusObjectPath> &) { refreshCardsSnapshot(); });
    connect(m_soundDBusInter, &SoundDBusProxy::SourcesChanged,
            this, [this](const QList<QDBusObjectPath> &) { refreshCardsSnapshot(); });

    connect(m_soundDBusInter, &SoundDBusProxy::PortEnabledChanged,
            this, &SoundWorker::onPortEnabledChanged);

    connect(m_mediaDevices, &QMediaDevices::audioInputsChanged,
            this, &SoundWorker::refreshCardsSnapshot);
    connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged,
            this, &SoundWorker::refreshCardsSnapshot);
    // ...
}

This keeps all current behavior but moves the business logic out of the constructor helper.

2. Headphone filtering logic in cardsChanged()

The new headphone logic adds an inline isHeadphonesPort lambda and a two‑pass loop structure. This is a good candidate for small helpers to keep cardsChanged() focused.

Suggestion: extract headphone helpers and reuse them in a single, simple pattern.

// soundworker.h
private:
    bool isHeadphonesPort(const QJsonObject &jPort) const;
    bool shouldFilterToHeadphonesOut(const QJsonArray &jPorts, uint cardId) const;
// soundworker.cpp
bool SoundWorker::isHeadphonesPort(const QJsonObject &jPort) const
{
    const QString portId = jPort["Name"].toString();
    const QString desc   = jPort["Description"].toString();
    return portId.contains("headphone", Qt::CaseInsensitive)
        || desc.contains("headphone", Qt::CaseInsensitive)
        || desc.contains(QStringLiteral("耳机"));
}

bool SoundWorker::shouldFilterToHeadphonesOut(const QJsonArray &jPorts, uint cardId) const
{
    for (const QJsonValue &pV : jPorts) {
        const QJsonObject jPort = pV.toObject();
        const auto dir = Port::Direction(jPort["Direction"].toDouble());
        if (dir != Port::Out || !isHeadphonesPort(jPort))
            continue;

        const double portAvai = jPort["Available"].toDouble();
        const QString portId  = jPort["Name"].toString();

        if (portAvai == 2.0) // Available
            return true;

        if (cardId == m_activeOutputCard && portId == m_activeSinkPort)
            return true;
    }
    return false;
}

Then in cardsChanged():

const bool filterToHeadphones = shouldFilterToHeadphonesOut(jPorts, cardId);

for (QJsonValue pV : jPorts) {
    QJsonObject jPort = pV.toObject();
    const double portAvai = jPort["Available"].toDouble();
    if (portAvai != 2.0 && portAvai != 0.0)
        continue;

    const auto dir = Port::Direction(jPort["Direction"].toDouble());
    if (filterToHeadphones && dir == Port::Out && !isHeadphonesPort(jPort))
        continue;

    const QString portId   = jPort["Name"].toString();
    const QString portName = jPort["Description"].toString();
    const bool isEnabled   = jPort["Enabled"].toBool();
    const bool isBluetooth = jPort["Bluetooth"].toBool();

    // existing Port creation/update logic...
}

This preserves all the new behavior (two‑phase decision: first decide if we filter, then apply it) but reduces nesting in cardsChanged() and clarifies intent via named helpers.

if (!m_soundDBusInter)
return;
// Trigger SoundModel::audioCardsChanged -> SoundWorker::cardsChanged to rebuild port list.
m_model->setAudioCards(m_soundDBusInter->cardsWithoutUnavailable());
};

connect(m_playAnimationTime, &QTimer::timeout, this, &SoundWorker::onAniTimerTimeOut);
connect(m_model, &SoundModel::defaultSinkChanged, this, &SoundWorker::defaultSinkChanged);
connect(m_model, &SoundModel::defaultSourceChanged, this, &SoundWorker::defaultSourceChanged);
Expand All @@ -63,6 +70,33 @@ void SoundWorker::initConnect()
connect(m_soundDBusInter, &SoundDBusProxy::MaxUIVolumeChanged, m_model, &SoundModel::setMaxUIVolume);
connect(m_soundDBusInter, &SoundDBusProxy::IncreaseVolumeChanged, m_model, &SoundModel::setIncreaseVolume);
connect(m_soundDBusInter, &SoundDBusProxy::CardsWithoutUnavailableChanged, m_model, &SoundModel::setAudioCards);
// Some backends only emit CardsChanged on port hotplug/availability updates.
// Treat it as a fallback to keep device list in sync.
connect(m_soundDBusInter, &SoundDBusProxy::CardsChanged, m_model, &SoundModel::setAudioCards);
// More fallbacks: hotplug may only change sink/source objects (PipeWire/PulseAudio).
connect(m_soundDBusInter, &SoundDBusProxy::SinksChanged, this, [refreshCards](const QList<QDBusObjectPath> &value) {
Q_UNUSED(value);
refreshCards();
});
connect(m_soundDBusInter, &SoundDBusProxy::SourcesChanged, this, [refreshCards](const QList<QDBusObjectPath> &value) {
Q_UNUSED(value);
refreshCards();
});
// Wired headset plug/unplug often changes port enabled state (not necessarily adding/removing cards).
connect(m_soundDBusInter, &SoundDBusProxy::PortEnabledChanged, this,
[this, refreshCards](uint cardId, const QString &portId, bool enabled) {
Port *port = m_model->findPort(portId, cardId);
if (!port) {
// If the port isn't present yet (e.g. previously unavailable), refresh card snapshot.
refreshCards();
return;
}

port->setEnabled(enabled);
m_model->updatePortCombo();
m_model->updateSoundDeviceModel(port);
m_model->updateActiveComboIndex();
});
connect(m_soundDBusInter, &SoundDBusProxy::ReduceNoiseChanged, m_model, &SoundModel::setReduceNoise);
connect(m_soundDBusInter, &SoundDBusProxy::PausePlayerChanged, m_model, &SoundModel::setPausePlayer);
connect(m_soundDBusInter, &SoundDBusProxy::BluetoothAudioModeOptsChanged, m_model, &SoundModel::setBluetoothAudioModeOpts);
Expand All @@ -71,6 +105,10 @@ void SoundWorker::initConnect()
connect(m_soundDBusInter, &SoundDBusProxy::EnabledChanged, m_model, &SoundModel::setEnableSoundEffect);
connect(m_soundDBusInter, &SoundDBusProxy::pendingCallWatcherFinished, this, &SoundWorker::getSoundEnabledMapFinished);

// Fallback: Qt multimedia device hotplug notifications (covers cases where DBus signals are missing).
connect(m_mediaDevices, &QMediaDevices::audioInputsChanged, this, refreshCards);
connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, refreshCards);

connect(m_pingTimer, &QTimer::timeout, [this] { if (m_soundDBusInter) m_soundDBusInter->Tick(); });
connect(m_soundDBusInter, &SoundDBusProxy::HasBatteryChanged, m_model, &SoundModel::setIsLaptop);

Expand Down Expand Up @@ -376,13 +414,56 @@ void SoundWorker::cardsChanged(const QString &cards)

QStringList tmpPorts;

// Laptop internal cards (e.g. sof-hda-dsp) may report both speaker and headphones as available.
// To match UX requirement: when headphones are plugged, only show the plugged device (Headphones).
const auto isHeadphonesPort = [](const QJsonObject &jPort) -> bool {
const QString portId = jPort["Name"].toString();
const QString desc = jPort["Description"].toString();
return portId.contains("headphone", Qt::CaseInsensitive)
|| desc.contains("headphone", Qt::CaseInsensitive)
|| desc.contains(QStringLiteral("耳机"));
};
// Prefer "active port" as the plug state signal, because on some laptops
// headphone port "Available" may stay 0 (Unknown) even when plugged.
bool shouldFilterToHeadphonesOut = false;
for (const QJsonValue &pV : jPorts) {
const QJsonObject jPort = pV.toObject();
const double portAvai = jPort["Available"].toDouble();
const auto dir = Port::Direction(jPort["Direction"].toDouble());
if (dir != Port::Out || !isHeadphonesPort(jPort)) {
continue;
}

const QString portId = jPort["Name"].toString();

// Case 1: backend reports explicit availability
if (portAvai == 2.0) { // 2 Available
shouldFilterToHeadphonesOut = true;
break;
}

// Case 2: use active sink port as ground truth for "plugged"
if (cardId == m_activeOutputCard && portId == m_activeSinkPort) {
shouldFilterToHeadphonesOut = true;
break;
}
}

for (QJsonValue pV : jPorts) {
QJsonObject jPort = pV.toObject();
const double portAvai = jPort["Available"].toDouble();
if (portAvai == 2.0 || portAvai == 0.0) { // 0 Unknown 1 Not available 2 Available
const QString portId = jPort["Name"].toString();
const QString portName = jPort["Description"].toString();
const auto dir = Port::Direction(jPort["Direction"].toDouble());
const bool isEnabled = jPort["Enabled"].toBool();

// If headphones are plugged/active on this card, hide other output ports (e.g. Speakers).
if (shouldFilterToHeadphonesOut) {
if (dir == Port::Out && !isHeadphonesPort(jPort)) {
continue;
}
}
const bool isBluetooth = jPort["Bluetooth"].toBool();

Port *port = m_model->findPort(portId, cardId);
Expand Down
Loading