mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-26 18:03:33 -04:00
475 lines
13 KiB
C++
475 lines
13 KiB
C++
/*
|
|
* PatchesDialog.cpp - display sf2 patches
|
|
*
|
|
* Copyright (c) 2008 Paul Giblock <drfaygo/at/gmail/dot/com>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* This program 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 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This program 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 this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*
|
|
*/
|
|
|
|
|
|
#include "PatchesDialog.h"
|
|
|
|
#include <fluidsynth.h>
|
|
#include <QHeaderView>
|
|
//#include <QFileInfo>
|
|
#include <QLabel>
|
|
#include <QKeyEvent>
|
|
#include <QSortFilterProxyModel>
|
|
#include <QStandardItemModel>
|
|
#include <QStandardItem>
|
|
|
|
#include "embed.h"
|
|
#include "fluidsynthshims.h"
|
|
|
|
namespace lmms::gui
|
|
{
|
|
|
|
|
|
// Custom list-view item (as for numerical sort purposes...)
|
|
class PatchItem : public QTreeWidgetItem
|
|
{
|
|
public:
|
|
|
|
// Constructor.
|
|
PatchItem( QTreeWidget *pListView,
|
|
QTreeWidgetItem *pItemAfter )
|
|
: QTreeWidgetItem( pListView, pItemAfter ) {}
|
|
|
|
// Sort/compare overriden method.
|
|
bool operator< ( const QTreeWidgetItem& other ) const override
|
|
{
|
|
int iColumn = QTreeWidgetItem::treeWidget()->sortColumn();
|
|
const QString& s1 = text( iColumn );
|
|
const QString& s2 = other.text( iColumn );
|
|
if( iColumn == 0 || iColumn == 2 )
|
|
{
|
|
return( s1.toInt() < s2.toInt() );
|
|
}
|
|
else
|
|
{
|
|
return( s1 < s2 );
|
|
}
|
|
}
|
|
};
|
|
|
|
// Constructor.
|
|
PatchesDialog::PatchesDialog( QWidget *pParent, Qt::WindowFlags wflags )
|
|
: QDialog(pParent, wflags)
|
|
, m_pSynth{nullptr}
|
|
, m_iChan{0}
|
|
, m_iBank{0}
|
|
, m_iProg{0}
|
|
, m_selProg{0}
|
|
{
|
|
// Setup UI struct...
|
|
setupUi( this );
|
|
|
|
// Configure bank list view
|
|
auto bankHeader = m_bankListView->header();
|
|
bankHeader->setSectionResizeMode(0, QHeaderView::Stretch);
|
|
bankHeader->setStretchLastSection(true);
|
|
bankHeader->resizeSection(0, 30);
|
|
|
|
m_splitter->setStretchFactor(0, 2);
|
|
m_splitter->setStretchFactor(1, 6);
|
|
|
|
// Configure program list models
|
|
m_progListSourceModel.setHorizontalHeaderLabels({tr("Patch"), tr("Name")});
|
|
m_progListProxyModel.setSourceModel(&m_progListSourceModel);
|
|
m_progListProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
m_progListProxyModel.setFilterKeyColumn(1); // "Name" column
|
|
m_progListProxyModel.setDynamicSortFilter(true);
|
|
|
|
// Configure program list view
|
|
m_progListView->setModel(&m_progListProxyModel);
|
|
m_progListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
m_progListView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
m_progListView->setSelectionMode(QAbstractItemView::SingleSelection);
|
|
m_progListView->setSortingEnabled(true);
|
|
m_progListView->sortByColumn(0, Qt::AscendingOrder); // Initial sort by column 0 (Name)
|
|
|
|
constexpr int RowHeight = 18;
|
|
auto progVHeader = m_progListView->verticalHeader();
|
|
progVHeader->setSectionResizeMode(QHeaderView::Fixed);
|
|
progVHeader->setMinimumSectionSize(RowHeight);
|
|
progVHeader->setMaximumSectionSize(RowHeight);
|
|
progVHeader->setDefaultSectionSize(RowHeight);
|
|
progVHeader->hide();
|
|
|
|
auto progHeader = m_progListView->horizontalHeader();
|
|
progHeader->setDefaultAlignment(Qt::AlignLeft);
|
|
progHeader->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
|
progHeader->setSectionResizeMode(1, QHeaderView::Stretch);
|
|
progHeader->setSectionsMovable(false);
|
|
progHeader->setStretchLastSection(true);
|
|
|
|
// Initial sort order...
|
|
m_bankListView->sortItems(0, Qt::AscendingOrder);
|
|
|
|
m_filterEdit->setPlaceholderText(tr("Search"));
|
|
m_filterEdit->setClearButtonEnabled(true);
|
|
m_filterEdit->addAction(embed::getIconPixmap("zoom"), QLineEdit::LeadingPosition);
|
|
|
|
// Configure focus (only allow for search bar and dialog buttons)
|
|
m_filterEdit->setFocus();
|
|
m_filterEdit->setFocusPolicy(Qt::StrongFocus);
|
|
m_bankListView->setFocusPolicy(Qt::NoFocus);
|
|
m_progListView->setFocusPolicy(Qt::NoFocus);
|
|
|
|
// UI connections...
|
|
QObject::connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString& text) {
|
|
m_progListProxyModel.setFilterRegularExpression(
|
|
QRegularExpression(text, QRegularExpression::CaseInsensitiveOption));
|
|
diffSelectProgRow(0); // fix the selection if it has been invalidated
|
|
});
|
|
QObject::connect(m_bankListView,
|
|
SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)),
|
|
SLOT(bankChanged()));
|
|
QObject::connect(m_progListView,
|
|
&QTableView::doubleClicked, this, &PatchesDialog::accept);
|
|
QObject::connect(m_progListView->selectionModel(), &QItemSelectionModel::currentRowChanged,
|
|
this, &PatchesDialog::progChanged);
|
|
QObject::connect(m_okButton,
|
|
SIGNAL(clicked()),
|
|
SLOT(accept()));
|
|
QObject::connect(m_cancelButton,
|
|
SIGNAL(clicked()),
|
|
SLOT(reject()));
|
|
}
|
|
|
|
|
|
// Dialog setup loader.
|
|
void PatchesDialog::setup ( fluid_synth_t * pSynth, int iChan,
|
|
const QString & _chanName,
|
|
LcdSpinBoxModel * _bankModel,
|
|
LcdSpinBoxModel * _progModel,
|
|
QLabel * _patchLabel )
|
|
{
|
|
// We'll going to changes the whole thing...
|
|
m_dirty = 0;
|
|
m_bankModel = _bankModel;
|
|
m_progModel = _progModel;
|
|
m_patchLabel = _patchLabel;
|
|
|
|
// Set the proper caption...
|
|
setWindowTitle( _chanName + " - Soundfont patches" );
|
|
|
|
// set m_pSynth to NULL so we don't trigger any progChanged events
|
|
m_pSynth = nullptr;
|
|
|
|
// Load bank list from actual synth stack...
|
|
m_bankListView->setSortingEnabled(false);
|
|
m_bankListView->clear();
|
|
|
|
// now it should be safe to set internal stuff
|
|
m_pSynth = pSynth;
|
|
m_iChan = iChan;
|
|
|
|
QTreeWidgetItem *pBankItem = nullptr;
|
|
// For all soundfonts (in reversed stack order) fill the available banks...
|
|
int cSoundFonts = ::fluid_synth_sfcount(m_pSynth);
|
|
for (int i = 0; i < cSoundFonts; i++) {
|
|
fluid_sfont_t *pSoundFont = ::fluid_synth_get_sfont(m_pSynth, i);
|
|
if (pSoundFont) {
|
|
#ifdef CONFIG_FLUID_BANK_OFFSET
|
|
int iBankOffset = ::fluid_synth_get_bank_offset(m_pSynth, fluid_sfont_get_id(pSoundFont));
|
|
#endif
|
|
fluid_sfont_iteration_start(pSoundFont);
|
|
#if FLUIDSYNTH_VERSION_MAJOR < 2
|
|
fluid_preset_t preset;
|
|
fluid_preset_t *pCurPreset = &preset;
|
|
#else
|
|
fluid_preset_t *pCurPreset = nullptr;
|
|
#endif
|
|
while ((pCurPreset = fluid_sfont_iteration_next_wrapper(pSoundFont, pCurPreset))) {
|
|
int iBank = fluid_preset_get_banknum(pCurPreset);
|
|
#ifdef CONFIG_FLUID_BANK_OFFSET
|
|
iBank += iBankOffset;
|
|
#endif
|
|
if (!findBankItem(iBank)) {
|
|
pBankItem = new PatchItem(m_bankListView, pBankItem);
|
|
if (pBankItem)
|
|
pBankItem->setText(0, QString::number(iBank));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
m_bankListView->setSortingEnabled(true);
|
|
|
|
// Set the selected bank.
|
|
m_iBank = 0;
|
|
fluid_preset_t *pPreset = ::fluid_synth_get_channel_preset(m_pSynth, m_iChan);
|
|
if (pPreset) {
|
|
m_iBank = fluid_preset_get_banknum(pPreset);
|
|
#ifdef CONFIG_FLUID_BANK_OFFSET
|
|
m_iBank += ::fluid_synth_get_bank_offset(m_pSynth, fluid_sfont_get_id(fluid_preset_get_sfont(sfont)));
|
|
#endif
|
|
}
|
|
|
|
pBankItem = findBankItem(m_iBank);
|
|
m_bankListView->setCurrentItem(pBankItem);
|
|
m_bankListView->scrollToItem(pBankItem);
|
|
bankChanged();
|
|
|
|
// Set the selected program.
|
|
if (pPreset) { m_iProg = fluid_preset_get_num(pPreset); }
|
|
|
|
if (auto progItem = findProgItem(m_iProg); progItem != nullptr)
|
|
{
|
|
auto sourceIdx = progItem->index();
|
|
auto proxyIdx = m_progListProxyModel.mapFromSource(sourceIdx);
|
|
|
|
if (proxyIdx.isValid())
|
|
{
|
|
constexpr auto setMask = QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows;
|
|
int row = proxyIdx.row();
|
|
auto idx = m_progListView->model()->index(row, 0);
|
|
|
|
m_progListView->selectionModel()->setCurrentIndex(idx, setMask);
|
|
m_progListView->scrollTo(idx);
|
|
}
|
|
}
|
|
|
|
// Done with setup...
|
|
//m_iDirtySetup--;
|
|
}
|
|
|
|
|
|
// Stabilize current state form.
|
|
void PatchesDialog::stabilizeForm()
|
|
{
|
|
m_okButton->setEnabled(validateForm());
|
|
}
|
|
|
|
|
|
// Validate form fields.
|
|
bool PatchesDialog::validateForm()
|
|
{
|
|
bool bValid = true;
|
|
|
|
bValid = bValid && (m_bankListView->currentItem() != nullptr);
|
|
|
|
return bValid;
|
|
}
|
|
|
|
|
|
// Realize a bank-program selection preset.
|
|
void PatchesDialog::setBankProg ( int iBank, int iProg )
|
|
{
|
|
if (m_pSynth == nullptr)
|
|
return;
|
|
|
|
// just select the synth's program preset...
|
|
::fluid_synth_bank_select(m_pSynth, m_iChan, iBank);
|
|
::fluid_synth_program_change(m_pSynth, m_iChan, iProg);
|
|
// Maybe this is needed to stabilize things around.
|
|
::fluid_synth_program_reset(m_pSynth);
|
|
}
|
|
|
|
|
|
// Validate form fields and accept it valid.
|
|
void PatchesDialog::accept()
|
|
{
|
|
if (validateForm()) {
|
|
bool updateUi = m_dirty > 0;
|
|
updatePatch(updateUi);
|
|
|
|
// Do remember preview state...
|
|
// if (m_pOptions)
|
|
// m_pOptions->bPresetPreview = m_ui.PreviewCheckBox->isChecked();
|
|
// We got it.
|
|
|
|
QDialog::accept();
|
|
}
|
|
}
|
|
|
|
|
|
// Reject settings (Cancel button slot).
|
|
void PatchesDialog::reject ()
|
|
{
|
|
// Reset selection to initial selection, if applicable...
|
|
if (m_dirty > 0)
|
|
setBankProg(m_bankModel->value(), m_progModel->value());
|
|
// Done (hopefully nothing).
|
|
QDialog::reject();
|
|
}
|
|
|
|
|
|
// Find the bank item of given bank number id.
|
|
QTreeWidgetItem *PatchesDialog::findBankItem ( int iBank )
|
|
{
|
|
QList<QTreeWidgetItem *> banks
|
|
= m_bankListView->findItems(
|
|
QString::number(iBank), Qt::MatchExactly, 0);
|
|
|
|
QListIterator<QTreeWidgetItem *> iter(banks);
|
|
if (iter.hasNext())
|
|
return iter.next();
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
QStandardItem* PatchesDialog::findProgItem(int iProg)
|
|
{
|
|
QList<QStandardItem*> progs = m_progListSourceModel.findItems(QString::number(iProg), Qt::MatchExactly, 0);
|
|
|
|
auto it = QListIterator<QStandardItem*>(progs);
|
|
return it.hasNext() ? it.next() : nullptr;
|
|
}
|
|
|
|
|
|
// Bank change slot.
|
|
void PatchesDialog::bankChanged ()
|
|
{
|
|
if (m_pSynth == nullptr)
|
|
return;
|
|
|
|
QTreeWidgetItem *pBankItem = m_bankListView->currentItem();
|
|
if (pBankItem == nullptr)
|
|
return;
|
|
|
|
int iBankSelected = pBankItem->text(0).toInt();
|
|
|
|
// Clear up the program list to refill
|
|
m_progListView->setSortingEnabled(false);
|
|
m_progListSourceModel.setRowCount(0);
|
|
|
|
// For all soundfonts (in reversed stack order) fill the available programs...
|
|
bool stop = false; // replaces the `pProgItem` check that used to exist here
|
|
int cSoundFonts = ::fluid_synth_sfcount(m_pSynth);
|
|
for (int i = 0; i < cSoundFonts && !stop; i++) {
|
|
fluid_sfont_t *pSoundFont = ::fluid_synth_get_sfont(m_pSynth, i);
|
|
if (pSoundFont) {
|
|
#ifdef CONFIG_FLUID_BANK_OFFSET
|
|
int iBankOffset = ::fluid_synth_get_bank_offset(m_pSynth, fluid_sfont_get_id(pSoundFont));
|
|
#endif
|
|
fluid_sfont_iteration_start(pSoundFont);
|
|
#if FLUIDSYNTH_VERSION_MAJOR < 2
|
|
fluid_preset_t preset;
|
|
fluid_preset_t *pCurPreset = &preset;
|
|
#else
|
|
fluid_preset_t *pCurPreset = nullptr;
|
|
#endif
|
|
while ((pCurPreset = fluid_sfont_iteration_next_wrapper(pSoundFont, pCurPreset))) {
|
|
int iBank = fluid_preset_get_banknum(pCurPreset);
|
|
#ifdef CONFIG_FLUID_BANK_OFFSET
|
|
iBank += iBankOffset;
|
|
#endif
|
|
int iProg = fluid_preset_get_num(pCurPreset);
|
|
if (iBank == iBankSelected && !findProgItem(iProg)) {
|
|
// Numeric value on the batch number column - allows for numerical sorting
|
|
auto patchNumItem = new QStandardItem();
|
|
patchNumItem->setData(iProg, Qt::DisplayRole);
|
|
|
|
auto patchNameItem = new QStandardItem(fluid_preset_get_name(pCurPreset));
|
|
|
|
stop = true;
|
|
|
|
m_progListSourceModel.appendRow({patchNumItem, patchNameItem});
|
|
// Old columns:
|
|
// Col. 2: QString::number(fluid_sfont_get_id(pSoundFont))
|
|
// Col. 3: QFileInfo(fluid_sfont_get_name(pSoundFont).baseName())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
m_progListView->setSortingEnabled(true);
|
|
|
|
// Stabilize the form.
|
|
stabilizeForm();
|
|
}
|
|
|
|
void PatchesDialog::updatePatch(bool updateUi)
|
|
{
|
|
int iBank = m_bankListView->currentItem()->text(0).toInt();
|
|
setBankProg(iBank, m_selProg);
|
|
|
|
if (updateUi)
|
|
{
|
|
m_bankModel->setValue(iBank);
|
|
m_progModel->setValue(m_selProg);
|
|
m_patchLabel->setText(m_selProgName);
|
|
}
|
|
}
|
|
|
|
void PatchesDialog::progChanged(const QModelIndex& cur, const QModelIndex& prev)
|
|
{
|
|
if (m_pSynth == nullptr) { return; }
|
|
|
|
auto curRow = m_progListProxyModel.mapToSource(cur).row();
|
|
if (curRow < 0) { return; }
|
|
|
|
auto progIdx = m_progListSourceModel.index(curRow, 0);
|
|
m_selProg = m_progListSourceModel.data(progIdx).toInt();
|
|
|
|
auto nameIdx = m_progListSourceModel.index(curRow, 1);
|
|
m_selProgName = m_progListSourceModel.data(nameIdx).toString();
|
|
|
|
// Which preview state...
|
|
if (validateForm())
|
|
{
|
|
updatePatch(false);
|
|
|
|
// Now we're dirty nuff.
|
|
m_dirty++;
|
|
}
|
|
|
|
// Stabilize the form.
|
|
stabilizeForm();
|
|
}
|
|
|
|
void PatchesDialog::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
const auto key = event->key();
|
|
|
|
if (key == Qt::Key_Up || key == Qt::Key_Down)
|
|
{
|
|
event->accept();
|
|
int rowDiff = (key == Qt::Key_Up) ? -1 : +1;
|
|
diffSelectProgRow(rowDiff);
|
|
}
|
|
else if (key == Qt::Key_Return || key == Qt::Key_Enter)
|
|
{
|
|
event->accept();
|
|
accept();
|
|
}
|
|
else if (key == Qt::Key_Escape)
|
|
{
|
|
event->accept();
|
|
reject();
|
|
}
|
|
}
|
|
|
|
void PatchesDialog::diffSelectProgRow(int offset)
|
|
{
|
|
QItemSelectionModel* selectionModel = m_progListView->selectionModel();
|
|
|
|
int curRow = selectionModel->currentIndex().row();
|
|
int newRow = curRow + offset;
|
|
int rowCount = m_progListView->model()->rowCount();
|
|
newRow = qBound(0, newRow, rowCount - 1);
|
|
|
|
constexpr auto selMask = QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows;
|
|
const auto idx = m_progListView->model()->index(newRow, 0);
|
|
selectionModel->setCurrentIndex(idx, selMask);
|
|
m_progListView->scrollTo(idx);
|
|
}
|
|
|
|
} // namespace lmms::gui
|