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

0001 /*
0002     This file is part of Akonadi
0003 
0004     SPDX-FileCopyrightText: 2014 Christian Mollekopf <mollekopf@kolabsys.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 #include "tageditwidget.h"
0009 #include "changerecorder.h"
0010 #include "tagattribute.h"
0011 #include "tagcreatejob.h"
0012 #include "tagdeletejob.h"
0013 #include "tagfetchscope.h"
0014 #include "tagmodel.h"
0015 #include "ui_tageditwidget.h"
0016 
0017 #include <KCheckableProxyModel>
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 
0021 #include <QEvent>
0022 #include <QPushButton>
0023 
0024 using namespace Akonadi;
0025 
0026 class Akonadi::TagEditWidgetPrivate : public QObject
0027 {
0028     Q_OBJECT
0029 public:
0030     explicit TagEditWidgetPrivate(QWidget *parent);
0031 
0032 public Q_SLOTS:
0033     void slotTextEdited(const QString &text);
0034     void slotItemEntered(const QModelIndex &index);
0035     void deleteTag();
0036     void slotCreateTag();
0037     void slotCreateTagFinished(KJob *job);
0038     void onRowsInserted(const QModelIndex &parent, int start, int end);
0039     void onModelPopulated();
0040 
0041 public:
0042     void initCheckableProxy(Akonadi::TagModel *model)
0043     {
0044         Q_ASSERT(m_checkableProxy);
0045 
0046         auto selectionModel = new QItemSelectionModel(model, m_checkableProxy.get());
0047         m_checkableProxy->setSourceModel(model);
0048         m_checkableProxy->setSelectionModel(selectionModel);
0049     }
0050 
0051     void select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag) const;
0052     enum ItemType {
0053         UrlTag = Qt::UserRole + 1,
0054     };
0055 
0056     QWidget *const d;
0057     Ui::TagEditWidget ui;
0058 
0059     Akonadi::Tag::List m_tags;
0060     Akonadi::TagModel *m_model = nullptr;
0061     QScopedPointer<KCheckableProxyModel> m_checkableProxy;
0062     QModelIndex m_deleteCandidate;
0063 
0064     QPushButton *m_deleteButton = nullptr;
0065 };
0066 
0067 TagEditWidgetPrivate::TagEditWidgetPrivate(QWidget *parent)
0068     : d(parent)
0069 {
0070 }
0071 
0072 void TagEditWidgetPrivate::select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag) const
0073 {
0074     if (!m_model) {
0075         return;
0076     }
0077 
0078     QItemSelection selection;
0079     for (int i = start; i <= end; i++) {
0080         const QModelIndex index = m_model->index(i, 0, parent);
0081         const auto insertedTag = index.data(Akonadi::TagModel::TagRole).value<Akonadi::Tag>();
0082         if (m_tags.contains(insertedTag)) {
0083             selection.select(index, index);
0084         }
0085     }
0086     if (m_checkableProxy) {
0087         m_checkableProxy->selectionModel()->select(selection, selectionFlag);
0088     }
0089 }
0090 
0091 void TagEditWidgetPrivate::onModelPopulated()
0092 {
0093     select(QModelIndex(), 0, m_model->rowCount() - 1, QItemSelectionModel::ClearAndSelect);
0094 }
0095 
0096 void TagEditWidgetPrivate::onRowsInserted(const QModelIndex &parent, int start, int end)
0097 {
0098     select(parent, start, end, QItemSelectionModel::Select);
0099 }
0100 
0101 void TagEditWidgetPrivate::slotCreateTag()
0102 {
0103     if (ui.newTagButton->isEnabled()) {
0104         auto createJob = new TagCreateJob(Akonadi::Tag(ui.newTagEdit->text()), this);
0105         connect(createJob, &TagCreateJob::finished, this, &TagEditWidgetPrivate::slotCreateTagFinished);
0106 
0107         ui.newTagEdit->clear();
0108         ui.newTagEdit->setEnabled(false);
0109         ui.newTagButton->setEnabled(false);
0110     }
0111 }
0112 
0113 void TagEditWidgetPrivate::slotCreateTagFinished(KJob *job)
0114 {
0115     if (job->error()) {
0116         KMessageBox::error(d, i18n("Failed to create a new tag"), i18nc("@title:window", "An Error Occurred while Creating a New Tag"));
0117     }
0118 
0119     ui.newTagEdit->setEnabled(true);
0120 }
0121 
0122 void TagEditWidgetPrivate::slotTextEdited(const QString &text)
0123 {
0124     // Remove unnecessary spaces from a new tag is
0125     // mandatory, as the user cannot see the difference
0126     // between a tag "Test" and "Test ".
0127     const QString tagText = text.simplified();
0128     if (tagText.isEmpty()) {
0129         ui.newTagButton->setEnabled(false);
0130         return;
0131     }
0132 
0133     // Check whether the new tag already exists
0134     bool exists = false;
0135     for (int i = 0, count = m_model->rowCount(); i < count; ++i) {
0136         const QModelIndex index = m_model->index(i, 0, QModelIndex());
0137         if (index.data(Qt::DisplayRole).toString() == tagText) {
0138             exists = true;
0139             break;
0140         }
0141     }
0142     ui.newTagButton->setEnabled(!exists);
0143 }
0144 
0145 void TagEditWidgetPrivate::slotItemEntered(const QModelIndex &index)
0146 {
0147     // align the delete-button to stay on the right border
0148     // of the item
0149     const QRect rect = ui.tagsView->visualRect(index);
0150     const int size = rect.height();
0151     const int x = rect.right() - size;
0152     const int y = rect.top();
0153     m_deleteButton->move(x, y);
0154     m_deleteButton->resize(size, size);
0155 
0156     m_deleteCandidate = index;
0157     m_deleteButton->show();
0158 }
0159 
0160 void TagEditWidgetPrivate::deleteTag()
0161 {
0162     Q_ASSERT(m_deleteCandidate.isValid());
0163     const auto tag = m_deleteCandidate.data(Akonadi::TagModel::TagRole).value<Akonadi::Tag>();
0164     const QString text = xi18nc("@info", "Do you really want to remove the tag <resource>%1</resource>?", tag.name());
0165     const QString caption = i18nc("@title:window", "Delete Tag");
0166     if (KMessageBox::questionTwoActions(d, text, caption, KStandardGuiItem::del(), KStandardGuiItem::cancel()) == KMessageBox::ButtonCode::PrimaryAction) {
0167         new TagDeleteJob(tag, this);
0168     }
0169 }
0170 
0171 TagEditWidget::TagEditWidget(QWidget *parent)
0172     : QWidget(parent)
0173     , d(new TagEditWidgetPrivate(this))
0174 {
0175     d->ui.setupUi(this);
0176 
0177     d->ui.tagsView->installEventFilter(this);
0178     connect(d->ui.tagsView, &QAbstractItemView::entered, d.get(), &TagEditWidgetPrivate::slotItemEntered);
0179 
0180     connect(d->ui.newTagEdit, &QLineEdit::textEdited, d.get(), &TagEditWidgetPrivate::slotTextEdited);
0181     connect(d->ui.newTagEdit, &QLineEdit::returnPressed, d.get(), &TagEditWidgetPrivate::slotCreateTag);
0182     connect(d->ui.newTagButton, &QAbstractButton::clicked, d.get(), &TagEditWidgetPrivate::slotCreateTag);
0183 
0184     // create the delete button, which is shown when
0185     // hovering the items
0186     d->m_deleteButton = new QPushButton(d->ui.tagsView->viewport());
0187     d->m_deleteButton->setObjectName(QLatin1StringView("tagDeleteButton"));
0188     d->m_deleteButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
0189     d->m_deleteButton->setToolTip(i18nc("@info", "Delete tag"));
0190     d->m_deleteButton->hide();
0191     connect(d->m_deleteButton, &QAbstractButton::clicked, d.get(), &TagEditWidgetPrivate::deleteTag);
0192 }
0193 
0194 TagEditWidget::TagEditWidget(Akonadi::TagModel *model, QWidget *parent, bool enableSelection)
0195     : TagEditWidget(parent)
0196 {
0197     setModel(model);
0198     setSelectionEnabled(enableSelection);
0199 }
0200 
0201 TagEditWidget::~TagEditWidget() = default;
0202 
0203 void TagEditWidget::setSelectionEnabled(bool enabled)
0204 {
0205     if (enabled == (d->m_checkableProxy != nullptr)) {
0206         return;
0207     }
0208 
0209     if (enabled) {
0210         d->m_checkableProxy.reset(new KCheckableProxyModel(this));
0211         if (d->m_model) {
0212             d->initCheckableProxy(d->m_model);
0213         }
0214         d->ui.tagsView->setModel(d->m_checkableProxy.get());
0215     } else {
0216         d->m_checkableProxy.reset();
0217         d->ui.tagsView->setModel(d->m_model);
0218     }
0219     d->ui.selectLabel->setVisible(enabled);
0220 }
0221 
0222 void TagEditWidget::setModel(TagModel *model)
0223 {
0224     if (d->m_model) {
0225         disconnect(d->m_model, &QAbstractItemModel::rowsInserted, d.get(), &TagEditWidgetPrivate::onRowsInserted);
0226         disconnect(d->m_model, &TagModel::populated, d.get(), &TagEditWidgetPrivate::onModelPopulated);
0227     }
0228 
0229     d->m_model = model;
0230     if (d->m_model) {
0231         connect(d->m_model, &QAbstractItemModel::rowsInserted, d.get(), &TagEditWidgetPrivate::onRowsInserted);
0232         if (d->m_checkableProxy) {
0233             d->initCheckableProxy(d->m_model);
0234             d->ui.tagsView->setModel(d->m_checkableProxy.get());
0235         } else {
0236             d->ui.tagsView->setModel(d->m_model);
0237         }
0238         connect(d->m_model, &TagModel::populated, d.get(), &TagEditWidgetPrivate::onModelPopulated);
0239     }
0240 }
0241 
0242 TagModel *TagEditWidget::model() const
0243 {
0244     return d->m_model;
0245 }
0246 
0247 bool TagEditWidget::selectionEnabled() const
0248 {
0249     return d->m_checkableProxy != nullptr;
0250 }
0251 
0252 void TagEditWidget::setSelection(const Akonadi::Tag::List &tags)
0253 {
0254     d->m_tags = tags;
0255     d->select(QModelIndex(), 0, d->m_model->rowCount() - 1, QItemSelectionModel::ClearAndSelect);
0256 }
0257 
0258 Akonadi::Tag::List TagEditWidget::selection() const
0259 {
0260     if (!d->m_checkableProxy) {
0261         return {};
0262     }
0263 
0264     Akonadi::Tag::List list;
0265     for (int i = 0; i < d->m_checkableProxy->rowCount(); ++i) {
0266         if (d->m_checkableProxy->selectionModel()->isRowSelected(i, QModelIndex())) {
0267             const auto index = d->m_checkableProxy->index(i, 0, QModelIndex());
0268             const auto tag = index.data(TagModel::TagRole).value<Tag>();
0269             list.push_back(tag);
0270         }
0271     }
0272     return list;
0273 }
0274 
0275 bool TagEditWidget::eventFilter(QObject *watched, QEvent *event)
0276 {
0277     if ((watched == d->ui.tagsView) && (event->type() == QEvent::Leave)) {
0278         d->m_deleteButton->hide();
0279     }
0280     return QWidget::eventFilter(watched, event);
0281 }
0282 
0283 #include "tageditwidget.moc"
0284 
0285 #include "moc_tageditwidget.cpp"