File indexing completed on 2024-05-19 04:27:46

0001 /*
0002  *    This file is part of the KDE project
0003  *    SPDX-FileCopyrightText: 2002 Patrick Julien <freak@codepimps.org>
0004  *    SPDX-FileCopyrightText: 2007 Jan Hambrecht <jaham@gmx.net>
0005  *    SPDX-FileCopyrightText: 2007 Sven Langkamp <sven.langkamp@gmail.com>
0006  *    SPDX-FileCopyrightText: 2011 Srikanth Tiyyagura <srikanth.tulasiram@gmail.com>
0007  *    SPDX-FileCopyrightText: 2011 José Luis Vergara <pentalis@gmail.com>
0008  *    SPDX-FileCopyrightText: 2013 Sascha Suelzer <s.suelzer@gmail.com>
0009  *    SPDX-FileCopyrightText: 2020 Agata Cacko <cacko.azh@gmail.com>
0010  *
0011  *    SPDX-License-Identifier: LGPL-2.0-or-later
0012  */
0013 
0014 #include "KisTagChooserWidget.h"
0015 
0016 #include <QDebug>
0017 #include <QToolButton>
0018 #include <QGridLayout>
0019 #include <QComboBox>
0020 #include <QMessageBox>
0021 
0022 #include <kconfig.h>
0023 #include <kconfiggroup.h>
0024 #include <ksharedconfig.h>
0025 #include <klocalizedstring.h>
0026 #include <kis_assert.h>
0027 #include <KisSqueezedComboBox.h>
0028 
0029 #include <KoIcon.h>
0030 
0031 #include "KisTagToolButton.h"
0032 #include <KisTagResourceModel.h>
0033 
0034 class Q_DECL_HIDDEN KisTagChooserWidget::Private
0035 {
0036 public:
0037     QComboBox *comboBox;
0038     KisTagToolButton *tagToolButton;
0039     KisTagModel *model;
0040     KisTagSP cachedTag;
0041     QString resourceType;
0042     QScopedPointer<KisTagModel> allTagsModel;
0043 };
0044 
0045 KisTagChooserWidget::KisTagChooserWidget(KisTagModel *model, QString resourceType, QWidget* parent)
0046     : QWidget(parent)
0047     , d(new Private)
0048 {
0049     d->resourceType = resourceType;
0050 
0051     d->comboBox = new QComboBox(this);
0052     d->comboBox->setToolTip(i18n("Tag"));
0053     d->comboBox->setSizePolicy(QSizePolicy::Policy::Expanding , QSizePolicy::Policy::Fixed);
0054 
0055     // Allow the combo box to not depend on content size.
0056     // Removing below code will cause the QComboBox when inside a QSplitter to have a width
0057     // equal to the longest QComboBox item regardless of size policy.
0058     d->comboBox->setMinimumContentsLength(1);
0059     d->comboBox->setSizeAdjustPolicy(QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon);
0060 
0061     d->comboBox->setInsertPolicy(QComboBox::InsertAlphabetically);
0062     model->sort(KisAllTagsModel::Name);
0063     d->comboBox->setModel(model);
0064 
0065     d->model = model;
0066     d->allTagsModel.reset(new KisTagModel(resourceType));
0067     d->allTagsModel->setTagFilter(KisTagModel::ShowAllTags);
0068 
0069     QGridLayout* comboLayout = new QGridLayout(this);
0070 
0071     comboLayout->addWidget(d->comboBox, 0, 0);
0072 
0073     d->tagToolButton = new KisTagToolButton(this);
0074     d->tagToolButton->setToolTip(i18n("Tag options"));
0075     comboLayout->addWidget(d->tagToolButton, 0, 1);
0076 
0077     comboLayout->setSpacing(0);
0078     comboLayout->setMargin(0);
0079     comboLayout->setColumnStretch(0, 3);
0080     this->setEnabled(true);
0081 
0082     connect(d->comboBox, SIGNAL(currentIndexChanged(int)),
0083             this, SLOT(tagChanged(int)));
0084 
0085     connect(d->tagToolButton, SIGNAL(popupMenuAboutToShow()),
0086             this, SLOT (tagToolContextMenuAboutToShow()));
0087 
0088     connect(d->tagToolButton, SIGNAL(newTagRequested(QString)),
0089             this, SLOT(addTag(QString)));
0090 
0091     connect(d->tagToolButton, SIGNAL(deletionOfCurrentTagRequested()),
0092             this, SLOT(tagToolDeleteCurrentTag()));
0093 
0094     connect(d->tagToolButton, SIGNAL(renamingOfCurrentTagRequested(const QString&)),
0095             this, SLOT(tagToolRenameCurrentTag(const QString&)));
0096 
0097     connect(d->tagToolButton, SIGNAL(undeletionOfTagRequested(KisTagSP)),
0098             this, SLOT(tagToolUndeleteLastTag(KisTagSP)));
0099 
0100 
0101     // Workaround for handling tag selection deselection when model resets.
0102     // Occurs when model changes under the user e.g. +/- a resource storage.
0103     connect(d->model, SIGNAL(modelAboutToBeReset()), this, SLOT(cacheSelectedTag()));
0104     connect(d->model, SIGNAL(modelReset()), this, SLOT(restoreTagFromCache()));
0105     connect(d->allTagsModel.data(), SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)),
0106             this, SLOT(slotTagModelDataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
0107 
0108 }
0109 
0110 KisTagChooserWidget::~KisTagChooserWidget()
0111 {
0112     delete d;
0113 }
0114 
0115 void KisTagChooserWidget::tagToolDeleteCurrentTag()
0116 {
0117     KisTagSP currentTag = currentlySelectedTag();
0118     if (!currentTag.isNull() && currentTag->id() >= 0) {
0119         d->model->setTagInactive(currentTag);
0120         setCurrentIndex(0);
0121         d->model->sort(KisAllTagsModel::Name);
0122     }
0123 }
0124 
0125 void KisTagChooserWidget::tagChanged(int tagIndex)
0126 {
0127     if (tagIndex >= 0) {
0128         KisTagSP tag = currentlySelectedTag();
0129         d->tagToolButton->setCurrentTag(tag);
0130         KConfigGroup group =  KSharedConfig::openConfig()->group("SelectedTags");
0131         group.writeEntry(d->resourceType, currentlySelectedTag()->url());
0132         d->model->sort(KisAllTagsModel::Name);
0133         emit sigTagChosen(tag);
0134     } else {
0135         setCurrentIndex(0);
0136     }
0137 }
0138 
0139 void KisTagChooserWidget::tagToolRenameCurrentTag(const QString& tagName)
0140 {
0141     KisTagSP tag = currentlySelectedTag();
0142     bool canRenameCurrentTag = !tag.isNull() && (tagName != tag->name());
0143 
0144     if (tagName == KisAllTagsModel::urlAll() || tagName == KisAllTagsModel::urlAllUntagged()) {
0145         QMessageBox::information(this, i18nc("Dialog title", "Can't rename the tag"), i18nc("Dialog message", "You can't use this name for your custom tags."), QMessageBox::Ok);
0146         return;
0147     }
0148 
0149     bool result = false;
0150 
0151     if (canRenameCurrentTag && !tagName.isEmpty()) {
0152          result = d->model->renameTag(tag, tagName, false);
0153 
0154         if (!result) {
0155             KisTagSP tagToRemove = d->model->tagForUrl(tagName);
0156 
0157             if (tagToRemove &&
0158                 QMessageBox::question(this, i18nc("Dialog title", "Remove existing tag with that name?"),
0159                 i18nc("Dialog message (the arguments are both somewhat user readable nouns or adjectives (names of the tags), can be treated as nouns since they represent the tags)",
0160                 "A tag with this unique name already exists. In order to continue renaming, the existing tag needs to be removed. Do you want to continue?\n"
0161                 "Tag to be removed: %1\n"
0162                 "Tag's unique name: %2", tagToRemove->name(), tagToRemove->url()), QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel) != QMessageBox::Cancel) {
0163                 result = d->model->renameTag(tag, tagName, true);
0164                 KIS_SAFE_ASSERT_RECOVER_RETURN(result);
0165             }
0166         }
0167     }
0168 
0169     if (result) {
0170         KisTagSP renamedTag = d->model->tagForUrl(tagName);
0171         KIS_SAFE_ASSERT_RECOVER_RETURN(renamedTag);
0172         const QModelIndex idx = d->model->indexForTag(renamedTag);
0173         setCurrentIndex(idx.row());
0174     }
0175 }
0176 
0177 void KisTagChooserWidget::tagToolUndeleteLastTag(KisTagSP tag)
0178 {
0179     int previousIndex = d->comboBox->currentIndex();
0180 
0181     bool success = d->model->setTagActive(tag);
0182     setCurrentIndex(previousIndex);
0183     if (success) {
0184         setCurrentItem(tag->name());
0185         d->model->sort(KisAllTagsModel::Name);
0186     }
0187 }
0188 
0189 void KisTagChooserWidget::cacheSelectedTag()
0190 {
0191     d->cachedTag = currentlySelectedTag();
0192 }
0193 
0194 void KisTagChooserWidget::restoreTagFromCache()
0195 {
0196     if (d->cachedTag) {
0197         QModelIndex cachedIndex = d->model->indexForTag(d->cachedTag);
0198         setCurrentIndex(cachedIndex.row());
0199         d->cachedTag = nullptr;
0200     }
0201 }
0202 
0203 void KisTagChooserWidget::slotTagModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> roles)
0204 {
0205     // we care only about the check status
0206     if (!roles.isEmpty() && !roles.contains(Qt::CheckStateRole)) {
0207         return;
0208     }
0209 
0210     const QModelIndex currIdx =
0211             d->allTagsModel->indexForTag(d->tagToolButton->undeletionCandidate());
0212 
0213     if (currIdx.isValid() &&
0214         currIdx.row() >= topLeft.row() && currIdx.row() <= bottomRight.row() &&
0215         currIdx.column() >= topLeft.column() && currIdx.column() <= bottomRight.column()) {
0216 
0217         const bool isNowActive = d->allTagsModel->data(currIdx, Qt::CheckStateRole).toBool();
0218 
0219         if (isNowActive) {
0220             d->tagToolButton->setUndeletionCandidate(KisTagSP());
0221         }
0222     }
0223 
0224     for (int row = topLeft.row(); row <= bottomRight.row(); row++) {
0225         for (int column = topLeft.column(); column <= bottomRight.column(); column++) {
0226             const QModelIndex idx = d->allTagsModel->index(row, column);
0227 
0228             const bool isActive = d->allTagsModel->data(idx, Qt::CheckStateRole).toBool();
0229 
0230             if (idx != currIdx && !isActive) {
0231                 d->tagToolButton->setUndeletionCandidate(d->allTagsModel->tagForIndex(idx));
0232                 break;
0233             }
0234         }
0235     }
0236 }
0237 
0238 void KisTagChooserWidget::setCurrentIndex(int index)
0239 {
0240     d->comboBox->setCurrentIndex(index);
0241 }
0242 
0243 int KisTagChooserWidget::currentIndex() const
0244 {
0245     return d->comboBox->currentIndex();
0246 }
0247 
0248 void KisTagChooserWidget::setCurrentItem(const QString &tag)
0249 {
0250     for (int i = 0; i < d->model->rowCount(); i++) {
0251         QModelIndex index = d->model->index(i, 0);
0252         QString currentRowTag = d->model->data(index, Qt::UserRole + KisAllTagsModel::Url).toString();
0253         if (currentRowTag == tag) {
0254             setCurrentIndex(i);
0255         }
0256     }
0257 }
0258 
0259 void KisTagChooserWidget::addTag(const QString &tag)
0260 {
0261     addTag(tag, 0);
0262 }
0263 
0264 KisTagChooserWidget::OverwriteDialogOptions KisTagChooserWidget::overwriteTagDialog(KisTagChooserWidget* parent, bool tagIsActive)
0265 {
0266     QString undeleteOption = !tagIsActive ? i18nc("Option in a dialog to undelete (reactivate) existing tag with its old assigned resources", "Restore previous tag")
0267                                       : i18nc("Option in a dialog to use existing tag with its old assigned resources", "Use existing tag");
0268     // if you use this simple cast, the order of buttons must match order of options in the enum
0269     return (KisTagChooserWidget::OverwriteDialogOptions)QMessageBox::question(parent, i18nc("Dialog title", "Overwrite tag?"), i18nc("Question to the user in a dialog about creating a tag",
0270                                                                                       "A tag with this unique name already exists. Do you want to replace it?"),
0271                                        i18nc("Option in a dialog to discard the previously existing tag and creating a new one in its place", "Replace (overwrite) tag"),
0272                                        undeleteOption, i18n("Cancel"));
0273 }
0274 
0275 void KisTagChooserWidget::addTag(const QString &tagName, KoResourceSP resource)
0276 {
0277     if (tagName == KisAllTagsModel::urlAll() || tagName == KisAllTagsModel::urlAllUntagged()) {
0278         QMessageBox::information(this, i18nc("Dialog title", "Can't create the tag"), i18nc("Dialog message", "You can't use this name for your custom tags."), QMessageBox::Ok);
0279         return;
0280     }
0281 
0282     if (tagName.isEmpty()) return;
0283 
0284     KisTagSP tagForUrl = d->model->tagForUrl(tagName);
0285     if (!tagForUrl.isNull()) {
0286         int response = overwriteTagDialog(this, tagForUrl->active());
0287         if (response == Undelete) { // Undelete
0288             d->model->setTagActive(tagForUrl);
0289             if (!resource.isNull()) {
0290                 KisTagResourceModel(d->resourceType).tagResources(tagForUrl, QVector<int>() << resource->resourceId());
0291             }
0292             d->model->sort(KisAllTagsModel::Name);
0293             return;
0294         } else if (response == Cancel) { // Cancel
0295             return;
0296         }
0297     }
0298     QVector<KoResourceSP> resources = (resource.isNull() ? QVector<KoResourceSP>() : (QVector<KoResourceSP>() << resource));
0299     d->model->addTag(tagName, true, resources); // this will overwrite the tag
0300     d->model->sort(KisAllTagsModel::Name);
0301 }
0302 
0303 void KisTagChooserWidget::addTag(KisTagSP tag, KoResourceSP resource)
0304 {
0305     if (tag->name() == KisAllTagsModel::urlAll() || tag->name() == KisAllTagsModel::urlAllUntagged()) {
0306         QMessageBox::information(this, i18nc("Dialog title", "Can't rename the tag"), i18nc("Dialog message", "You can't use this name for your custom tags."), QMessageBox::Ok);
0307         return;
0308     }
0309 
0310     KisTagSP tagForUrl = d->model->tagForUrl(tag->url());
0311     if (!tagForUrl.isNull()) {
0312         int response = overwriteTagDialog(this, tagForUrl->active());
0313         if (response == Undelete) { // Undelete
0314             d->model->setTagActive(tagForUrl);
0315             if (!resource.isNull()) {
0316                 KisTagResourceModel(d->resourceType).tagResources(tagForUrl, QVector<int>() << resource->resourceId());
0317             }
0318             d->model->sort(KisAllTagsModel::Name);
0319             return;
0320         } else if (response == Cancel) { // Cancel
0321             return;
0322         }
0323     }
0324     QVector<KoResourceSP> resources = (resource.isNull() ? QVector<KoResourceSP>() : (QVector<KoResourceSP>() << resource));
0325     d->model->addTag(tag, true, resources); // this will overwrite the tag
0326     d->model->sort(KisAllTagsModel::Name);
0327 }
0328 
0329 KisTagSP KisTagChooserWidget::currentlySelectedTag()
0330 {
0331     int row = d->comboBox->currentIndex();
0332     if (row < 0) {
0333         return nullptr;
0334     }
0335 
0336     QModelIndex index = d->model->index(row, 0);
0337     KisTagSP tag =  d->model->tagForIndex(index);
0338     return tag;
0339 }
0340 
0341 void KisTagChooserWidget::updateIcons()
0342 {
0343     d->tagToolButton->loadIcon();
0344 }
0345 
0346 void KisTagChooserWidget::tagToolContextMenuAboutToShow()
0347 {
0348     /* only enable the save button if the selected tag set is editable */
0349     if (currentlySelectedTag()) {
0350         d->tagToolButton->readOnlyMode(currentlySelectedTag()->id() < 0);
0351     }
0352     else {
0353         d->tagToolButton->readOnlyMode(true);
0354     }
0355 }