File indexing completed on 2024-04-28 15:40:28
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"