Skip to content
Open
Show file tree
Hide file tree
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
103 changes: 103 additions & 0 deletions src/data/PresetManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
#include "model/PresetListModel.h"

#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>

PresetManager::PresetManager() : _presetModel(new PresetListModel(this))
{
loadRules();
loadPresetDevices();
}

bool PresetManager::exists(const QString &name) const
Expand All @@ -38,6 +40,11 @@ bool PresetManager::loadFromPath(const QString &filename)

QFile::copy(src, dest);
DspConfig::instance().load();

// If this preset remembers an output device, switch to it. The preset name is
// the file's base name; presets without an association leave the device untouched.
applyPresetDevice(QFileInfo(filename).completeBaseName());

Log::debug("Loaded " + filename);
return true;
}
Expand All @@ -55,6 +62,14 @@ void PresetManager::rename(const QString &name, const QString &newName)
{
QFile::rename(path, QDir(path).filePath(newName + ".conf"));
}

// Keep the device association in sync with the preset's new name
if (_presetDevices.contains(name))
{
_presetDevices[newName] = _presetDevices.take(name);
savePresetDevices();
}

this->_presetModel->rescan();
}

Expand All @@ -64,6 +79,7 @@ bool PresetManager::remove(const QString &name)
if (QFile::exists(path))
{
QFile::remove(path);
clearPresetDevice(name);
this->_presetModel->rescan();
return true;
}
Expand Down Expand Up @@ -96,7 +112,11 @@ void PresetManager::onOutputDeviceChanged(const QString &deviceName, const QStri
{
QString defaultRouteId = QString::fromStdString(RouteListModel::makeDefaultRoute().name);
auto executeRule = [this, deviceName, outputRouteId, defaultRouteId](const PresetRule& rule){
// A rule loads a preset *because* the device changed; do not let the loaded
// preset re-apply a device, which would feed back into this handler.
_suppressDeviceApply = true;
loadFromPath(AppConfig::instance().getPath("presets/" + rule.preset + ".conf"));
_suppressDeviceApply = false;
emit presetAutoloaded(deviceName, rule.routeName, rule.routeId == defaultRouteId);
};

Expand Down Expand Up @@ -203,3 +223,86 @@ QVector<PresetRule> PresetManager::rules() const
return _rules;
}

QString PresetManager::presetDevicesPath() const
{
return AppConfig::instance().getPath("preset_devices.json");
}

void PresetManager::loadPresetDevices()
{
_presetDevices.clear();

QFile json(presetDevicesPath());
if(!json.exists())
{
return;
}

json.open(QFile::ReadOnly);
QJsonObject root = QJsonDocument::fromJson(json.readAll()).object();
json.close();

for(auto it = root.constBegin(); it != root.constEnd(); ++it)
{
_presetDevices.insert(it.key(), it.value().toObject());
}
}

void PresetManager::savePresetDevices()
{
QFile json(presetDevicesPath());
if(!json.open(QIODevice::WriteOnly)){
Log::error("PresetManager::savePresetDevices: Cannot open json file");
return;
}

QJsonObject root;
for(auto it = _presetDevices.constBegin(); it != _presetDevices.constEnd(); ++it)
{
root.insert(it.key(), it.value());
}

json.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
json.close();
}

bool PresetManager::hasPresetDevice(const QString &name) const
{
return _presetDevices.contains(name);
}

void PresetManager::setPresetDevice(const QString &name)
{
QJsonObject entry;
entry["deviceId"] = AppConfig::instance().get<QString>(AppConfig::AudioOutputDevice);
entry["useDefault"] = AppConfig::instance().get<bool>(AppConfig::AudioOutputUseDefault);
_presetDevices[name] = entry;
savePresetDevices();
}

void PresetManager::clearPresetDevice(const QString &name)
{
if(_presetDevices.remove(name) > 0)
{
savePresetDevices();
}
}

void PresetManager::applyPresetDevice(const QString &name)
{
if(_suppressDeviceApply || !_presetDevices.contains(name))
{
return;
}

const QJsonObject entry = _presetDevices.value(name);

// Mirror SettingsFragment: setting these AppConfig keys triggers a live device
// switch via PipewireAudioService::onAppConfigUpdated.
AppConfig::instance().set(AppConfig::AudioOutputUseDefault, entry["useDefault"].toBool());
if(!entry["useDefault"].toBool())
{
AppConfig::instance().set(AppConfig::AudioOutputDevice, entry["deviceId"].toString());
}
}

16 changes: 16 additions & 0 deletions src/data/PresetManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "PresetRule.h"

#include <QJsonObject>
#include <QMap>
#include <QObject>
#include <QVector>

Expand All @@ -29,6 +31,13 @@ class PresetManager : public QObject
bool addRule(const PresetRule& rule);
void removeRule(const QString &deviceId, const QString &routeId);

// Per-preset output device association (PipeWire only).
// Lets a preset optionally remember an output device and switch to it on load.
bool hasPresetDevice(const QString& name) const;
void setPresetDevice(const QString& name);
void clearPresetDevice(const QString& name);
void applyPresetDevice(const QString& name);

PresetListModel *presetModel() const;

signals:
Expand All @@ -49,9 +58,16 @@ public slots:
void saveRules() const;
private:
QVector<PresetRule> _rules;
QMap<QString, QJsonObject> _presetDevices;
PresetListModel* _presetModel;
// Suppresses applyPresetDevice() while a preset is being auto-loaded by a
// device->preset rule, to avoid a device-change feedback loop.
bool _suppressDeviceApply = false;

QString rulesPath() const;
QString presetDevicesPath() const;
void loadPresetDevices();
void savePresetDevices();
};

#endif // PRESETMANAGER_H
21 changes: 18 additions & 3 deletions src/interface/fragment/PresetFragment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ PresetFragment::PresetFragment(IAudioService* service, QWidget *parent) :
ui->load->setEnabled(false);

#ifdef USE_PULSEAUDIO
// Device switching is only implemented for the PipeWire backend
ui->rules->setVisible(false);
ui->rememberDevice->setVisible(false);
#endif

connect(ui->add, &QPushButton::clicked, this, &PresetFragment::onAddClicked);
Expand Down Expand Up @@ -58,7 +60,9 @@ void PresetFragment::onSelectionChanged(const QItemSelection &selected, const QI
return;
}

ui->presetName->setText(PresetManager::instance().presetModel()->data(selected.indexes().first(), Qt::UserRole).toString());
const QString selectedName = PresetManager::instance().presetModel()->data(selected.indexes().first(), Qt::UserRole).toString();
ui->presetName->setText(selectedName);
ui->rememberDevice->setChecked(PresetManager::instance().hasPresetDevice(selectedName));
}

void PresetFragment::onAddClicked()
Expand All @@ -68,8 +72,19 @@ void PresetFragment::onAddClicked()
return;
}

PresetManager::instance().save(ui->presetName->text());
ui->presetName->text() = "";
const QString name = ui->presetName->text();
PresetManager::instance().save(name);

if(ui->rememberDevice->isChecked())
{
PresetManager::instance().setPresetDevice(name);
}
else
{
PresetManager::instance().clearPresetDevice(name);
}

ui->presetName->clear();
}

void PresetFragment::onRemoveClicked()
Expand Down
10 changes: 10 additions & 0 deletions src/interface/fragment/PresetFragment.ui
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="rememberDevice">
<property name="toolTip">
<string>Save the current output device with this preset and switch to it when the preset is loaded</string>
</property>
<property name="text">
<string>Remember output device</string>
</property>
</widget>
</item>
<item>
<widget class="CListView" name="files">
<property name="contextMenuPolicy">
Expand Down