From 6c75b30dca08fad6923930381db77886e02508c8 Mon Sep 17 00:00:00 2001 From: Yohanan <23298480+yohannd1@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:57:19 -0300 Subject: [PATCH] Add search bar for the soundfont player (#8193) --- plugins/Sf2Player/PatchesDialog.cpp | 247 +++++++++++++++++++--------- plugins/Sf2Player/PatchesDialog.h | 55 ++++--- plugins/Sf2Player/PatchesDialog.ui | 37 +---- 3 files changed, 209 insertions(+), 130 deletions(-) diff --git a/plugins/Sf2Player/PatchesDialog.cpp b/plugins/Sf2Player/PatchesDialog.cpp index 8a32a10ce..2874a8a3a 100644 --- a/plugins/Sf2Player/PatchesDialog.cpp +++ b/plugins/Sf2Player/PatchesDialog.cpp @@ -29,7 +29,12 @@ #include //#include #include +#include +#include +#include +#include +#include "embed.h" #include "fluidsynthshims.h" namespace lmms::gui @@ -63,45 +68,83 @@ public: } }; - - // Constructor. PatchesDialog::PatchesDialog( QWidget *pParent, Qt::WindowFlags wflags ) - : QDialog( pParent, wflags ) + : QDialog(pParent, wflags) + , m_pSynth{nullptr} + , m_iChan{0} + , m_iBank{0} + , m_iProg{0} + , m_selProg{0} { // Setup UI struct... setupUi( this ); - m_pSynth = nullptr; - m_iChan = 0; - m_iBank = 0; - m_iProg = 0; + // Configure bank list view + auto bankHeader = m_bankListView->header(); + bankHeader->setSectionResizeMode(0, QHeaderView::Stretch); + bankHeader->setStretchLastSection(true); + bankHeader->resizeSection(0, 30); - // Soundfonts list view... - QHeaderView *pHeader = m_progListView->header(); -// pHeader->setResizeMode(QHeaderView::Custom); - pHeader->setDefaultAlignment(Qt::AlignLeft); -// pHeader->setDefaultSectionSize(200); - pHeader->setSectionsMovable(false); - pHeader->setStretchLastSection(true); + m_splitter->setStretchFactor(0, 2); + m_splitter->setStretchFactor(1, 6); - m_progListView->resizeColumnToContents(0); // Prog. - //pHeader->resizeSection(1, 200); // Name. + // 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_progListView->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, - SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), - SLOT(progChanged(QTreeWidgetItem*,QTreeWidgetItem*))); - QObject::connect(m_progListView, - SIGNAL(itemActivated(QTreeWidgetItem*,int)), - SLOT(accept())); + &QTableView::doubleClicked, this, &PatchesDialog::accept); + QObject::connect(m_progListView->selectionModel(), &QItemSelectionModel::currentRowChanged, + this, &PatchesDialog::progChanged); QObject::connect(m_okButton, SIGNAL(clicked()), SLOT(accept())); @@ -118,7 +161,6 @@ void PatchesDialog::setup ( fluid_synth_t * pSynth, int iChan, LcdSpinBoxModel * _progModel, QLabel * _patchLabel ) { - // We'll going to changes the whole thing... m_dirty = 0; m_bankModel = _bankModel; @@ -139,7 +181,6 @@ void PatchesDialog::setup ( fluid_synth_t * pSynth, int iChan, 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); @@ -187,11 +228,23 @@ void PatchesDialog::setup ( fluid_synth_t * pSynth, int iChan, bankChanged(); // Set the selected program. - if (pPreset) - m_iProg = fluid_preset_get_num(pPreset); - QTreeWidgetItem *pProgItem = findProgItem(m_iProg); - m_progListView->setCurrentItem(pProgItem); - m_progListView->scrollToItem(pProgItem); + 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--; @@ -211,7 +264,6 @@ bool PatchesDialog::validateForm() bool bValid = true; bValid = bValid && (m_bankListView->currentItem() != nullptr); - bValid = bValid && (m_progListView->currentItem() != nullptr); return bValid; } @@ -235,23 +287,14 @@ void PatchesDialog::setBankProg ( int iBank, int iProg ) void PatchesDialog::accept() { if (validateForm()) { - // Unload from current selected dialog items. - int iBank = (m_bankListView->currentItem())->text(0).toInt(); - int iProg = (m_progListView->currentItem())->text(0).toInt(); - // And set it right away... - setBankProg(iBank, iProg); - - if (m_dirty > 0) { - m_bankModel->setValue( iBank ); - m_progModel->setValue( iProg ); - m_patchLabel->setText( m_progListView-> - currentItem()->text( 1 ) ); - } + 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(); } } @@ -282,23 +325,15 @@ QTreeWidgetItem *PatchesDialog::findBankItem ( int iBank ) return nullptr; } - -// Find the program item of given program number id. -QTreeWidgetItem *PatchesDialog::findProgItem ( int iProg ) +QStandardItem* PatchesDialog::findProgItem(int iProg) { - QList progs - = m_progListView->findItems( - QString::number(iProg), Qt::MatchExactly, 0); + QList progs = m_progListSourceModel.findItems(QString::number(iProg), Qt::MatchExactly, 0); - QListIterator iter(progs); - if (iter.hasNext()) - return iter.next(); - else - return nullptr; + auto it = QListIterator(progs); + return it.hasNext() ? it.next() : nullptr; } - // Bank change slot. void PatchesDialog::bankChanged () { @@ -311,13 +346,14 @@ void PatchesDialog::bankChanged () int iBankSelected = pBankItem->text(0).toInt(); - // Clear up the program listview. + // Clear up the program list to refill m_progListView->setSortingEnabled(false); - m_progListView->clear(); - QTreeWidgetItem *pProgItem = nullptr; + 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 && !pProgItem; i++) { + 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 @@ -337,14 +373,18 @@ void PatchesDialog::bankChanged () #endif int iProg = fluid_preset_get_num(pCurPreset); if (iBank == iBankSelected && !findProgItem(iProg)) { - pProgItem = new PatchItem(m_progListView, pProgItem); - if (pProgItem) { - pProgItem->setText(0, QString::number(iProg)); - pProgItem->setText(1, fluid_preset_get_name(pCurPreset)); - //pProgItem->setText(2, QString::number(fluid_sfont_get_id(pSoundFont))); - //pProgItem->setText(3, QFileInfo( - // fluid_sfont_get_name(pSoundFont).baseName()); - } + // 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()) } } } @@ -355,20 +395,37 @@ void PatchesDialog::bankChanged () stabilizeForm(); } - -// Program change slot. -void PatchesDialog::progChanged (QTreeWidgetItem * _curr, QTreeWidgetItem * _prev) +void PatchesDialog::updatePatch(bool updateUi) { - if (m_pSynth == nullptr || _curr == nullptr) - return; + 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() ) { - // Set current selection. - int iBank = (m_bankListView->currentItem())->text(0).toInt(); - int iProg = _curr->text(0).toInt(); - // And set it right away... - setBankProg(iBank, iProg); + if (validateForm()) + { + updatePatch(false); + // Now we're dirty nuff. m_dirty++; } @@ -377,5 +434,41 @@ void PatchesDialog::progChanged (QTreeWidgetItem * _curr, QTreeWidgetItem * _pre 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 diff --git a/plugins/Sf2Player/PatchesDialog.h b/plugins/Sf2Player/PatchesDialog.h index 5309ce0bf..3d45f7199 100644 --- a/plugins/Sf2Player/PatchesDialog.h +++ b/plugins/Sf2Player/PatchesDialog.h @@ -27,6 +27,9 @@ #define _PATCHES_DIALOG_H #include +#include +#include +#include #include "ui_PatchesDialog.h" #include "LcdSpinBox.h" @@ -45,25 +48,18 @@ class PatchesDialog : public QDialog, private Ui::PatchesDialog Q_OBJECT public: - - // Constructor. - PatchesDialog(QWidget *pParent = 0, Qt::WindowFlags wflags = QFlag(0)); - - // Destructor. + PatchesDialog(QWidget* pParent = 0, Qt::WindowFlags wflags = QFlag(0)); ~PatchesDialog() override = default; - - void setup(fluid_synth_t *pSynth, int iChan, const QString & _chanName, - LcdSpinBoxModel * _bankModel, LcdSpinBoxModel * _progModel, QLabel *_patchLabel ); + void setup(fluid_synth_t* pSynth, int iChan, const QString& _chanName, LcdSpinBoxModel* _bankModel, + LcdSpinBoxModel* _progModel, QLabel* _patchLabel); public slots: - void stabilizeForm(); void bankChanged(); - void progChanged( QTreeWidgetItem * _curr, QTreeWidgetItem * _prev ); + void progChanged(const QModelIndex& cur, const QModelIndex& prev); protected slots: - void accept() override; void reject() override; @@ -71,27 +67,44 @@ protected: void setBankProg(int iBank, int iProg); - QTreeWidgetItem *findBankItem(int iBank); - QTreeWidgetItem *findProgItem(int iProg); + QTreeWidgetItem* findBankItem(int iBank); + + //! Finds the program item of given program number id in the source model. + QStandardItem* findProgItem(int iProg); bool validateForm(); + /** + Updates the current patch, and updates the UI controls if `updateUi` is true. + */ + void updatePatch(bool updateUi); + + /** + Selects a row in the program selector based off a signed offset from the currently selected row. Also clamps the + selection. + */ + void diffSelectProgRow(int offset); + private: - // Instance variables. - fluid_synth_t *m_pSynth; + void keyPressEvent(QKeyEvent* event) override; + fluid_synth_t* m_pSynth; int m_iChan; int m_iBank; int m_iProg; - - //int m_iDirtySetup; - //int m_iDirtyCount; int m_dirty; + // int m_iDirtySetup; + // int m_iDirtyCount; - LcdSpinBoxModel * m_bankModel; - LcdSpinBoxModel * m_progModel; - QLabel *m_patchLabel; + int m_selProg; + QString m_selProgName; + + LcdSpinBoxModel* m_bankModel; + LcdSpinBoxModel* m_progModel; + QLabel* m_patchLabel; + QStandardItemModel m_progListSourceModel; //!< Programs on the selected bank + QSortFilterProxyModel m_progListProxyModel; //!< Model to allow searching }; diff --git a/plugins/Sf2Player/PatchesDialog.ui b/plugins/Sf2Player/PatchesDialog.ui index 93fd67c50..35d1d26c6 100644 --- a/plugins/Sf2Player/PatchesDialog.ui +++ b/plugins/Sf2Player/PatchesDialog.ui @@ -39,6 +39,10 @@ Qsynth: Channel Preset + + + + @@ -93,41 +97,10 @@ - + Program selector - - true - - - 4 - - - false - - - true - - - false - - - true - - - true - - - - Patch - - - - - Name - -