File indexing completed on 2024-04-28 04:21:26

0001 // SPDX-FileCopyrightText: 2023 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0002 // SPDX-FileCopyrightText: 2024 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "SelectCategoryAndValue.h"
0007 #include "ui_SelectCategoryAndValue.h"
0008 #include <DB/CategoryCollection.h>
0009 #include <DB/ImageDB.h>
0010 
0011 #include <KLocalizedString>
0012 #include <QCompleter>
0013 #include <QIdentityProxyModel>
0014 #include <QMenu>
0015 #include <QPushButton>
0016 #include <QStandardItem>
0017 
0018 #include <algorithm>
0019 
0020 Q_DECLARE_METATYPE(DB::CategoryPtr)
0021 
0022 namespace
0023 {
0024 /**
0025     This proxy model adapts its filtering according to the current search.
0026     This is to allow the search to match "Person / Jesper" by simply searching for "Per Je"
0027 */
0028 class AdaptiveFilterProxy : public QIdentityProxyModel
0029 {
0030     Q_OBJECT
0031 public:
0032     using QIdentityProxyModel::QIdentityProxyModel;
0033 
0034     static constexpr int SearchRole = 2000;
0035     void setText(const QString &text)
0036     {
0037         if (text == m_text)
0038             return;
0039         m_text = text;
0040         m_subStrings = text.split(QLatin1String(" "));
0041         // This signal must be emitted to invalidate the completor, and force it to filter its rows
0042         // again.
0043         Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), { SearchRole });
0044     }
0045 
0046     QVariant data(const QModelIndex &index, int role) const override
0047     {
0048         if (role != SearchRole)
0049             return QIdentityProxyModel::data(index, role);
0050 
0051         auto matches = [itemText = data(index, Qt::DisplayRole).toString()](const QString &str) {
0052             return itemText.contains(str, Qt::CaseInsensitive);
0053         };
0054 
0055         if (!m_text.isEmpty() && std::all_of(m_subStrings.cbegin(), m_subStrings.cend(), matches)) {
0056             // If the item matches then simply return the search string which will make the
0057             // completer include this match.
0058             return m_text;
0059         } else {
0060             // Otherwise return an empty string which will make the completer discard this item.
0061             return QString();
0062         }
0063     }
0064 
0065 private:
0066     QStringList m_subStrings;
0067     QString m_text;
0068 };
0069 
0070 enum Role { Category = Qt::UserRole,
0071             Item };
0072 
0073 } // namespace
0074 
0075 SelectCategoryAndValue::SelectCategoryAndValue(const QString &title, const QString &message, const Viewer::AnnotationHandler::Assignments &assignments, QWidget *parent)
0076     : QDialog(parent)
0077     , ui(new Ui::SelectCategoryAndValue)
0078 {
0079     ui->setupUi(this);
0080     setWindowTitle(title);
0081     ui->label->setText(message);
0082     setupExistingAssignments(assignments);
0083 
0084     auto model = new QStandardItemModel(this);
0085     int row = 0;
0086 
0087     const auto sortOrder = DB::ImageDB::instance()->categoryCollection()->globalSortOrder()->completeSortOrder();
0088     for (const auto &item : sortOrder) {
0089         auto modelItem = new QStandardItem(QLatin1String("%1 / %2").arg(item.category, item.item));
0090         modelItem->setData(item.category, Role::Category);
0091         modelItem->setData(item.item, Role::Item);
0092         model->insertRow(row++, modelItem);
0093     }
0094 
0095     auto searchFilter = new AdaptiveFilterProxy(this);
0096     searchFilter->setSourceModel(model);
0097 
0098     auto completer = new QCompleter(this);
0099     ui->lineEdit->setCompleter(completer);
0100     completer->setModel(searchFilter);
0101     completer->setFilterMode(Qt::MatchStartsWith);
0102     completer->setCompletionMode(QCompleter::PopupCompletion);
0103     completer->setCompletionRole(AdaptiveFilterProxy::SearchRole);
0104     completer->setCaseSensitivity(Qt::CaseInsensitive);
0105     connect(ui->lineEdit, &QLineEdit::textChanged, searchFilter, &AdaptiveFilterProxy::setText);
0106 
0107     ui->lineEdit->setPlaceholderText(i18n("Type name of category and item"));
0108 
0109     connect(ui->lineEdit, &QLineEdit::returnPressed, this, [this, completer] {
0110         auto index = completer->currentIndex();
0111         if (index.isValid()) {
0112             m_category = index.data(Role::Category).toString();
0113             m_item = index.data(Role::Item).toString();
0114         }
0115         accept();
0116     });
0117 
0118     connect(
0119         completer, qOverload<const QModelIndex &>(&QCompleter::activated), this,
0120         [this](const QModelIndex &index) {
0121             m_category = index.data(Role::Category).toString();
0122             m_item = index.data(Role::Item).toString().trimmed();
0123             accept();
0124         });
0125 
0126     connect(ui->lineEdit, &QLineEdit::textChanged, ui->buttonBox, [completer, this] {
0127         ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(completer->currentIndex().isValid());
0128     });
0129 
0130     connect(ui->value, &QLineEdit::textChanged, ui->buttonBox, [this](const QString &txt) {
0131         ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!txt.isEmpty());
0132     });
0133 
0134     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0135 
0136     ui->addNewTag->setText(i18n("Add New"));
0137     connect(ui->addNewTag, &QPushButton::clicked, this, &SelectCategoryAndValue::addNew);
0138     ui->categoryLabel->hide();
0139     ui->category->hide();
0140     ui->titleLabel->hide();
0141     ui->value->hide();
0142 
0143     connect(ui->buttonBox, &QDialogButtonBox::helpRequested, this, &SelectCategoryAndValue::helpRequest);
0144 
0145     const auto categories = DB::ImageDB::instance()->categoryCollection()->categories();
0146     for (const auto &category : categories) {
0147         if (!category->isSpecialCategory())
0148             ui->category->addItem(category->name(), QVariant::fromValue(category));
0149     }
0150 
0151     ui->knownAssignments->setContextMenuPolicy(Qt::CustomContextMenu);
0152     connect(ui->knownAssignments, &QWidget::customContextMenuRequested, this, &SelectCategoryAndValue::knownAssignmentsContextMenu);
0153 }
0154 
0155 SelectCategoryAndValue::~SelectCategoryAndValue() = default;
0156 
0157 int SelectCategoryAndValue::exec()
0158 {
0159     auto result = QDialog::exec();
0160     if (result == QDialog::Rejected)
0161         return result;
0162 
0163     if (!ui->category->isHidden()) {
0164         m_category = ui->category->currentText();
0165         m_item = ui->value->text();
0166         ui->category->currentData().value<DB::CategoryPtr>()->addItem(m_item);
0167     }
0168 
0169     DB::ImageDB::instance()->categoryCollection()->globalSortOrder()->pushToFront(m_category, m_item);
0170     return result;
0171 }
0172 
0173 void SelectCategoryAndValue::addNew()
0174 {
0175     ui->lineEdit->hide();
0176     ui->addNewTag->hide();
0177     ui->categoryLabel->show();
0178     ui->category->show();
0179     ui->titleLabel->show();
0180     ui->value->show();
0181     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
0182     ui->value->setText(ui->lineEdit->text());
0183     ui->category->setFocus();
0184 }
0185 
0186 void SelectCategoryAndValue::setupExistingAssignments(const Viewer::AnnotationHandler::Assignments &assignments)
0187 {
0188     auto model = new QStandardItemModel(this);
0189     model->setHorizontalHeaderLabels(QStringList { i18n("Key"), i18n("Tag") });
0190     int row = 0;
0191     auto addAssignment = [&](const QString &key, const Viewer::AnnotationHandler::Assignment &assignment) {
0192         auto *keyItem = new QStandardItem(key);
0193         keyItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
0194         model->setItem(row, 0, keyItem);
0195         auto *assignmentItem = new QStandardItem(QLatin1String("%1 / %2").arg(assignment.category, assignment.value));
0196         assignmentItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
0197         model->setItem(row, 1, assignmentItem);
0198         ++row;
0199     };
0200 
0201     for (auto it = assignments.cbegin(); it != assignments.cend(); ++it) {
0202         const Viewer::AnnotationHandler::Assignment assignment = it.value();
0203         addAssignment(it.key(), assignment);
0204     }
0205 
0206     ui->knownAssignments->setModel(model);
0207     ui->knownAssignments->verticalHeader()->hide();
0208     ui->knownAssignments->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
0209     ui->knownAssignments->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
0210     ui->knownAssignments->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
0211 }
0212 
0213 void SelectCategoryAndValue::knownAssignmentsContextMenu(const QPoint &point)
0214 {
0215     const auto &index = ui->knownAssignments->indexAt(point);
0216     if (index.isValid()) {
0217         QMenu contextMenu { this };
0218         QAction deleteItemAction { this };
0219         contextMenu.addAction(&deleteItemAction);
0220         deleteItemAction.setText(i18nc("@action:inmenu", "Clear assignment"));
0221         connect(&deleteItemAction, &QAction::triggered, ui->knownAssignments, [&] {
0222             // initiate removal from config:
0223             const auto assignmentKey = index.siblingAtColumn(0);
0224             if (assignmentKey.isValid()) {
0225                 // clear assignment:
0226                 const auto key = ui->knownAssignments->model()->data(assignmentKey).toString();
0227                 Q_EMIT keyRemovalRequested(key);
0228 
0229                 // update table view:
0230                 ui->knownAssignments->model()->removeRow(index.row());
0231             }
0232         });
0233         contextMenu.exec(ui->knownAssignments->mapToGlobal(point));
0234     }
0235 }
0236 
0237 QString SelectCategoryAndValue::category() const
0238 {
0239     return m_category;
0240 }
0241 
0242 QString SelectCategoryAndValue::value() const
0243 {
0244     return m_item;
0245 }
0246 
0247 #include "SelectCategoryAndValue.moc"