File indexing completed on 2025-01-05 04:47:11

0001 /*
0002   SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
0003   SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
0004 
0005   SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "tagselectioncombobox.h"
0009 
0010 #include "monitor.h"
0011 #include "tagmodel.h"
0012 
0013 #include <KCheckableProxyModel>
0014 #include <QAbstractItemView>
0015 #include <QEvent>
0016 #include <QItemSelectionModel>
0017 #include <QKeyEvent>
0018 #include <QLineEdit>
0019 #include <QLocale>
0020 
0021 #include <KLocalizedString>
0022 
0023 #include <algorithm>
0024 #include <variant>
0025 
0026 using namespace Akonadi;
0027 
0028 namespace
0029 {
0030 template<typename List>
0031 List tagsFromSelection(const QItemSelection &selection, int role)
0032 {
0033     List tags;
0034     for (int i = 0; i < selection.size(); ++i) {
0035         const auto indexes = selection.at(i).indexes();
0036         std::transform(indexes.cbegin(), indexes.cend(), std::back_inserter(tags), [role](const auto &idx) {
0037             return idx.model()->data(idx, role).template value<typename List::value_type>();
0038         });
0039     }
0040     return tags;
0041 }
0042 
0043 QString getEditText(const QItemSelection &selection)
0044 {
0045     const auto tags = tagsFromSelection<Tag::List>(selection, TagModel::TagRole);
0046     QStringList names;
0047     names.reserve(tags.size());
0048     std::transform(tags.cbegin(), tags.cend(), std::back_inserter(names), std::bind(&Tag::name, std::placeholders::_1));
0049     return QLocale{}.createSeparatedList(names);
0050 }
0051 
0052 } // namespace
0053 
0054 class Akonadi::TagSelectionComboBoxPrivate
0055 {
0056 public:
0057     explicit TagSelectionComboBoxPrivate(TagSelectionComboBox *parent)
0058         : q(parent)
0059     {
0060     }
0061 
0062     enum LoopControl {
0063         Break,
0064         Continue,
0065     };
0066 
0067     template<typename Selection, typename Comp>
0068     void setSelection(const Selection &entries, Comp &&cmp)
0069     {
0070         if (!mModelReady) {
0071             mPendingSelection = entries;
0072             return;
0073         }
0074 
0075         const auto forEachIndex = [this, entries, cmp](auto &&func) {
0076             for (int i = 0, cnt = tagModel->rowCount(); i < cnt; ++i) {
0077                 const auto index = tagModel->index(i, 0);
0078                 const auto tag = tagModel->data(index, TagModel::TagRole).value<Tag>();
0079                 if (std::any_of(entries.cbegin(), entries.cend(), std::bind(cmp, tag, std::placeholders::_1))) {
0080                     if (func(index) == Break) {
0081                         break;
0082                     }
0083                 }
0084             }
0085         };
0086 
0087         if (mCheckable) {
0088             QItemSelection selection;
0089             forEachIndex([&selection](const QModelIndex &index) {
0090                 selection.push_back(QItemSelectionRange{index});
0091                 return Continue;
0092             });
0093             selectionModel->select(selection, QItemSelectionModel::ClearAndSelect);
0094         } else {
0095             forEachIndex([this](const QModelIndex &index) {
0096                 q->setCurrentIndex(index.row());
0097                 return Break;
0098             });
0099         }
0100     }
0101 
0102     void toggleItem(const QModelIndex &tagModelIndex) const
0103     {
0104         selectionModel->select(tagModelIndex, QItemSelectionModel::Toggle);
0105     }
0106 
0107     void setItemChecked(const QModelIndex &tagModelIndex, Qt::CheckState state) const
0108     {
0109         selectionModel->select(tagModelIndex, state == Qt::Checked ? QItemSelectionModel::Select : QItemSelectionModel::Deselect);
0110     }
0111 
0112     void setCheckable(bool checkable)
0113     {
0114         if (checkable) {
0115             selectionModel = std::make_unique<QItemSelectionModel>(tagModel.get(), q);
0116             checkableProxy = std::make_unique<KCheckableProxyModel>(q);
0117             checkableProxy->setSourceModel(tagModel.get());
0118             checkableProxy->setSelectionModel(selectionModel.get());
0119 
0120             tagModel->setParent(nullptr);
0121             q->setModel(checkableProxy.get());
0122             tagModel->setParent(q);
0123 
0124             q->setEditable(true);
0125             q->lineEdit()->setReadOnly(true);
0126             q->lineEdit()->setPlaceholderText(i18nc("@label Placeholder text in tag selection combobox", "Select tags..."));
0127             q->lineEdit()->setAlignment(Qt::AlignLeft);
0128 
0129             q->lineEdit()->installEventFilter(q);
0130             q->view()->installEventFilter(q);
0131             q->view()->viewport()->installEventFilter(q);
0132 
0133             q->connect(selectionModel.get(), &QItemSelectionModel::selectionChanged, q, [this]() {
0134                 const auto selection = selectionModel->selection();
0135                 q->setEditText(getEditText(selection));
0136                 Q_EMIT q->selectionChanged(tagsFromSelection<Tag::List>(selection, TagModel::TagRole));
0137             });
0138             q->connect(q, &QComboBox::activated, selectionModel.get(), [this](int i) {
0139                 if (q->view()->isVisible()) {
0140                     const auto index = tagModel->index(i, 0);
0141                     toggleItem(index);
0142                 }
0143             });
0144         } else {
0145             // QComboBox automatically deletes models that it is a parent of
0146             // which breaks our stuff
0147             tagModel->setParent(nullptr);
0148             q->setModel(tagModel.get());
0149             tagModel->setParent(q);
0150 
0151             if (q->lineEdit()) {
0152                 q->lineEdit()->removeEventFilter(q);
0153             }
0154             if (q->view()) {
0155                 q->view()->removeEventFilter(q);
0156                 q->view()->viewport()->removeEventFilter(q);
0157             }
0158 
0159             q->setEditable(false);
0160 
0161             selectionModel.reset();
0162             checkableProxy.reset();
0163         }
0164     }
0165 
0166     std::unique_ptr<QItemSelectionModel> selectionModel;
0167     std::unique_ptr<TagModel> tagModel;
0168     std::unique_ptr<KCheckableProxyModel> checkableProxy;
0169 
0170     bool mCheckable = false;
0171     bool mAllowHide = true;
0172     bool mModelReady = false;
0173 
0174     std::variant<std::monostate, Tag::List, QStringList> mPendingSelection;
0175 
0176 private:
0177     TagSelectionComboBox *const q;
0178 };
0179 
0180 TagSelectionComboBox::TagSelectionComboBox(QWidget *parent)
0181     : QComboBox(parent)
0182     , d(new TagSelectionComboBoxPrivate(this))
0183 {
0184     auto monitor = new Monitor(this);
0185     monitor->setObjectName(QLatin1StringView("TagSelectionComboBoxMonitor"));
0186     monitor->setTypeMonitored(Monitor::Tags);
0187 
0188     d->tagModel = std::make_unique<TagModel>(monitor, this);
0189     connect(d->tagModel.get(), &TagModel::populated, this, [this]() {
0190         d->mModelReady = true;
0191         if (auto list = std::get_if<Tag::List>(&d->mPendingSelection)) {
0192             setSelection(*list);
0193         } else if (auto slist = std::get_if<QStringList>(&d->mPendingSelection)) {
0194             setSelection(*slist);
0195         }
0196         d->mPendingSelection = std::monostate{};
0197     });
0198 
0199     d->setCheckable(d->mCheckable);
0200 }
0201 
0202 TagSelectionComboBox::~TagSelectionComboBox() = default;
0203 
0204 void TagSelectionComboBox::setCheckable(bool checkable)
0205 {
0206     if (d->mCheckable != checkable) {
0207         d->mCheckable = checkable;
0208         d->setCheckable(d->mCheckable);
0209     }
0210 }
0211 
0212 bool TagSelectionComboBox::checkable() const
0213 {
0214     return d->mCheckable;
0215 }
0216 
0217 void TagSelectionComboBox::setSelection(const Tag::List &tags)
0218 {
0219     d->setSelection(tags, [](const Tag &a, const Tag &b) {
0220         return a.name() == b.name();
0221     });
0222 }
0223 
0224 void TagSelectionComboBox::setSelection(const QStringList &tagNames)
0225 {
0226     d->setSelection(tagNames, [](const Tag &a, const QString &b) {
0227         return a.name() == b;
0228     });
0229 }
0230 
0231 Tag::List TagSelectionComboBox::selection() const
0232 {
0233     if (!d->selectionModel) {
0234         return {currentData(TagModel::TagRole).value<Tag>()};
0235     }
0236     return tagsFromSelection<Tag::List>(d->selectionModel->selection(), TagModel::TagRole);
0237 }
0238 
0239 QStringList TagSelectionComboBox::selectionNames() const
0240 {
0241     if (!d->selectionModel) {
0242         return {currentText()};
0243     }
0244     return tagsFromSelection<QStringList>(d->selectionModel->selection(), TagModel::NameRole);
0245 }
0246 
0247 void TagSelectionComboBox::hidePopup()
0248 {
0249     if (d->mAllowHide) {
0250         QComboBox::hidePopup();
0251     }
0252     d->mAllowHide = true;
0253 }
0254 
0255 void TagSelectionComboBox::keyPressEvent(QKeyEvent *event)
0256 {
0257     switch (event->key()) {
0258     case Qt::Key_Up:
0259     case Qt::Key_Down:
0260         showPopup();
0261         event->accept();
0262         break;
0263     case Qt::Key_Return:
0264     case Qt::Key_Enter:
0265     case Qt::Key_Escape:
0266         hidePopup();
0267         event->accept();
0268         break;
0269     default:
0270         break;
0271     }
0272 }
0273 
0274 bool TagSelectionComboBox::eventFilter(QObject *receiver, QEvent *event)
0275 {
0276     switch (event->type()) {
0277     case QEvent::KeyPress:
0278     case QEvent::KeyRelease:
0279     case QEvent::ShortcutOverride:
0280         switch (static_cast<QKeyEvent *>(event)->key()) {
0281         case Qt::Key_Return:
0282         case Qt::Key_Enter:
0283         case Qt::Key_Escape:
0284             hidePopup();
0285             return true;
0286         }
0287         break;
0288     case QEvent::MouseButtonDblClick:
0289     case QEvent::MouseButtonPress:
0290     case QEvent::MouseButtonRelease:
0291         d->mAllowHide = false;
0292         if (receiver == lineEdit()) {
0293             showPopup();
0294             return true;
0295         }
0296         break;
0297     default:
0298         break;
0299     }
0300     return QComboBox::eventFilter(receiver, event);
0301 }
0302 
0303 #include "moc_tagselectioncombobox.cpp"