Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MIDI Input editor: allow selecting multiple Options #12348

Merged
merged 4 commits into from
May 22, 2024
Merged
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
124 changes: 106 additions & 18 deletions src/controllers/delegates/midioptionsdelegate.cpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
#include "controllers/delegates/midioptionsdelegate.h"

#include <QAbstractItemView>
#include <QComboBox>
#include <QStandardItemModel>

#include "controllers/midi/midimessage.h"
#include "controllers/midi/midiutils.h"
#include "moc_midioptionsdelegate.cpp"
#include "util/parented_ptr.h"

namespace {

const QList<MidiOption> kMidiOptions = {
MidiOption::None,
// Don't add 'Normal' to the list because it's useless: it's exclusive,
// meaning it's the implicit result of unchecking all other options, but
// clicking it does not uncheck all other options. Also, showing it
// checked is pointless (and it's not updated if others are checked).
// Furthermore, the mapping list is cleaner without it, mappings that
// have options set are much easier to spot.
// MidiOption::None,
MidiOption::Invert,
MidiOption::Rot64,
MidiOption::Rot64Invert,
Expand All @@ -35,18 +44,49 @@ MidiOptionsDelegate::MidiOptionsDelegate(QObject* pParent)
MidiOptionsDelegate::~MidiOptionsDelegate() {
}


QWidget* MidiOptionsDelegate::createEditor(QWidget* parent,
const QStyleOptionViewItem& option,
const QModelIndex& index) const {
Q_UNUSED(option);
Q_UNUSED(index);
QComboBox* pComboBox = new QComboBox(parent);

for (const MidiOption choice : kMidiOptions) {
pComboBox->addItem(MidiUtils::midiOptionToTranslatedString(choice),
static_cast<uint16_t>(choice));
// Create, populate and connect the box.
QComboBox* pComboBox = make_parented<QComboBox>(parent);
auto* pModel = static_cast<QStandardItemModel*>(pComboBox->model());
DEBUG_ASSERT(pModel);
for (const MidiOption opt : kMidiOptions) {
QStandardItem* pItem =
new QStandardItem(MidiUtils::midiOptionToTranslatedString(opt));
pItem->setData(static_cast<uint16_t>(opt));
pItem->setCheckable(true);
pModel->appendRow(pItem);
}
// Add a special item to uncheck all. See commitAndCloseEditor()
QStandardItem* pItem = new QStandardItem(tr("Unset all"));
pItem->setCheckable(false); // doesn't hurt to set this explicitly
pModel->appendRow(pItem);

// Unsetting the index clears the display text which is visible when closing
// the list view by clicking anywhere else. Default text is that of first
// added item. It can be set to any combination of existing item texts, e.g.
// all checked items, but it's not updated before the selection is committed
// so let's simply clear the text to avoid confusion.
pComboBox->setCurrentIndex(-1);

// Default horizontal size policy is Preferred which results in center elide
// as soon as items are checkable, regardless the elide mode.
pComboBox->view()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
pComboBox->view()->setTextElideMode(Qt::ElideNone);

// * clicking an option or pressing Enter on a selected option toggles it,
// closes the list view and commits the updated data
// * pressing Space on a selected option toggles it, list remains open
// * clicking outside the listview closes it, and another click causing the
// combobox to lose focus closes that and commits pending changes
connect(pComboBox,
QOverload<int>::of(&QComboBox::activated),
this,
&MidiOptionsDelegate::commitAndCloseEditor);

return pComboBox;
}
Expand All @@ -66,28 +106,76 @@ QString MidiOptionsDelegate::displayText(const QVariant& value,

void MidiOptionsDelegate::setEditorData(QWidget* editor,
const QModelIndex& index) const {
MidiOptions options = index.data(Qt::EditRole).value<MidiOptions>();

QComboBox* pComboBox = qobject_cast<QComboBox*>(editor);
if (pComboBox == nullptr) {
auto* pComboBox = qobject_cast<QComboBox*>(editor);
if (!pComboBox) {
return;
}
for (int i = 0; i < pComboBox->count(); ++i) {
if (MidiOptions(pComboBox->itemData(i).toInt()) & options) {
pComboBox->setCurrentIndex(i);
return;

// Update checked states
const MidiOptions options = index.data(Qt::EditRole).value<MidiOptions>();
const auto* pModel = static_cast<QStandardItemModel*>(pComboBox->model());
DEBUG_ASSERT(pModel);
for (int row = 0; row < pModel->rowCount(); row++) {
auto* pItem = pModel->item(row, 0);
if (!pItem->isCheckable()) {
continue;
}
auto opt = static_cast<MidiOption>(pItem->data().toInt());
pItem->setCheckState(options.testFlag(opt) ? Qt::Checked : Qt::Unchecked);
}

// Show popup immediately, as with the other editors, no 'edit' click
// required to open the list view.
pComboBox->showPopup();
}

void MidiOptionsDelegate::setModelData(QWidget* editor,
QAbstractItemModel* model,
const QModelIndex& index) const {
MidiOptions options;
QComboBox* pComboBox = qobject_cast<QComboBox*>(editor);
if (pComboBox == nullptr) {
// Collect checked options and write them back to the model
auto* pComboBox = qobject_cast<QComboBox*>(editor);
if (!pComboBox) {
return;
}
options = MidiOptions(pComboBox->itemData(pComboBox->currentIndex()).toInt());

const auto* pModel = static_cast<QStandardItemModel*>(pComboBox->model());
DEBUG_ASSERT(pModel);
MidiOptions options;
for (int i = 0; i < pModel->rowCount(); i++) {
auto* pItem = pModel->item(i);
// Only check for Qt::Checked or else, ignore Qt::PartiallyChecked
options.setFlag(static_cast<MidiOption>(pItem->data().toUInt()),
pItem->checkState() == Qt::Checked);
}
model->setData(index, QVariant::fromValue(options), Qt::EditRole);
}

void MidiOptionsDelegate::commitAndCloseEditor(int index) {
QComboBox* pComboBox = qobject_cast<QComboBox*>(sender());
if (!pComboBox) {
return;
}
const auto* pModel = static_cast<QStandardItemModel*>(pComboBox->model());
DEBUG_ASSERT(pModel);
auto* pItem = pModel->item(index);
DEBUG_ASSERT(pItem);
if (pItem->isCheckable()) {
pItem->setCheckState(pItem->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked);
// TODO Concurrent option scan be selected. Implement a compatibility
// matrix and uncheck all options that are incompatible with the last
// checked option. Store initial check state for/in each item and
// hook up to QStandardItemModel::itemChanged()
} else {
// Clear was selected. Uncheck all other items
for (int row = 0; row < pModel->rowCount() - 1; row++) {
if (row == index) { // Actually it's the last item, but this is safer.
continue;
}
pItem = pModel->item(row, 0);
pItem->setCheckState(Qt::Unchecked);
}
}

emit commitData(pComboBox);
emit closeEditor(pComboBox);
}
3 changes: 3 additions & 0 deletions src/controllers/delegates/midioptionsdelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ class MidiOptionsDelegate : public QStyledItemDelegate {

void setModelData(QWidget* editor, QAbstractItemModel* model,
const QModelIndex& index) const;

private slots:
void commitAndCloseEditor(int index);
};