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

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "semanticinfocontextmanageritem.h"
0023 
0024 // Qt
0025 #include <QAction>
0026 #include <QDialog>
0027 #include <QEvent>
0028 #include <QPainter>
0029 #include <QShortcut>
0030 #include <QStyle>
0031 #include <QTimer>
0032 #include <QVBoxLayout>
0033 
0034 // KF
0035 #include <KActionCategory>
0036 #include <KActionCollection>
0037 #include <KIconLoader>
0038 #include <KLocalizedString>
0039 #include <KRatingPainter>
0040 #include <KSharedConfig>
0041 #include <KWindowConfig>
0042 
0043 // Local
0044 #include "sidebar.h"
0045 #include "ui_semanticinfodialog.h"
0046 #include "ui_semanticinfosidebaritem.h"
0047 #include "viewmainpage.h"
0048 #include <lib/contextmanager.h>
0049 #include <lib/decoratedtag/decoratedtag.h>
0050 #include <lib/documentview/documentview.h>
0051 #include <lib/eventwatcher.h>
0052 #include <lib/flowlayout.h>
0053 #include <lib/hud/hudwidget.h>
0054 #include <lib/semanticinfo/semanticinfodirmodel.h>
0055 #include <lib/semanticinfo/sorteddirmodel.h>
0056 #include <lib/signalblocker.h>
0057 #include <lib/widgetfloater.h>
0058 
0059 namespace Gwenview
0060 {
0061 static const int RATING_INDICATOR_HIDE_DELAY = 2000;
0062 
0063 struct SemanticInfoDialog : public QDialog, public Ui_SemanticInfoDialog {
0064     SemanticInfoDialog(QWidget *parent)
0065         : QDialog(parent)
0066     {
0067         setLayout(new QVBoxLayout);
0068         auto mainWidget = new QWidget;
0069         layout()->addWidget(mainWidget);
0070         setupUi(mainWidget);
0071         mainWidget->layout()->setContentsMargins(0, 0, 0, 0);
0072         setWindowTitle(mainWidget->windowTitle());
0073 
0074         KWindowConfig::restoreWindowSize(windowHandle(), configGroup());
0075     }
0076 
0077     ~SemanticInfoDialog() override
0078     {
0079         KConfigGroup group = configGroup();
0080         KWindowConfig::saveWindowSize(windowHandle(), group);
0081     }
0082 
0083     KConfigGroup configGroup() const
0084     {
0085         KSharedConfigPtr config = KSharedConfig::openConfig();
0086         return KConfigGroup(config, "SemanticInfoDialog");
0087     }
0088 };
0089 
0090 /**
0091  * A QGraphicsPixmapItem-like class, but which inherits from QGraphicsWidget
0092  */
0093 class GraphicsPixmapWidget : public QGraphicsWidget
0094 {
0095 public:
0096     void setPixmap(const QPixmap &pix)
0097     {
0098         mPix = pix;
0099         setMinimumSize(pix.size());
0100     }
0101 
0102     void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
0103     {
0104         painter->drawPixmap((size().width() - mPix.width()) / 2, (size().height() - mPix.height()) / 2, mPix);
0105     }
0106 
0107 private:
0108     QPixmap mPix;
0109 };
0110 
0111 class RatingIndicator : public HudWidget
0112 {
0113 public:
0114     RatingIndicator()
0115         : HudWidget()
0116         , mPixmapWidget(new GraphicsPixmapWidget)
0117         , mDeleteTimer(new QTimer(this))
0118     {
0119         updatePixmap(0);
0120         setOpacity(0);
0121         init(mPixmapWidget, OptionNone);
0122 
0123         mDeleteTimer->setInterval(RATING_INDICATOR_HIDE_DELAY);
0124         mDeleteTimer->setSingleShot(true);
0125         connect(mDeleteTimer, &QTimer::timeout, this, &HudWidget::fadeOut);
0126         connect(this, &HudWidget::fadedOut, this, &QObject::deleteLater);
0127     }
0128 
0129     void setRating(int rating)
0130     {
0131         updatePixmap(rating);
0132         update();
0133         mDeleteTimer->start();
0134         fadeIn();
0135     }
0136 
0137 private:
0138     GraphicsPixmapWidget *mPixmapWidget = nullptr;
0139     QTimer *mDeleteTimer = nullptr;
0140 
0141     void updatePixmap(int rating)
0142     {
0143         KRatingPainter ratingPainter;
0144         const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Small);
0145         QPixmap pix(iconSize * 5 + ratingPainter.spacing() * 4, iconSize);
0146         pix.fill(Qt::transparent);
0147         {
0148             QPainter painter(&pix);
0149             ratingPainter.paint(&painter, pix.rect(), rating);
0150         }
0151         mPixmapWidget->setPixmap(pix);
0152     }
0153 };
0154 
0155 struct SemanticInfoContextManagerItemPrivate : public Ui_SemanticInfoSideBarItem {
0156     SemanticInfoContextManagerItem *q;
0157     SideBarGroup *mGroup;
0158     KActionCollection *mActionCollection;
0159     ViewMainPage *mViewMainPage;
0160     QPointer<SemanticInfoDialog> mSemanticInfoDialog;
0161     TagInfo mTagInfo;
0162     QAction *mEditTagsAction;
0163     /** A list of all actions, so that we can disable them when necessary */
0164     QList<QAction *> mActions;
0165     QPointer<RatingIndicator> mRatingIndicator;
0166     FlowLayout *mTagLayout;
0167     QLabel *mEditLabel;
0168 
0169     void setupGroup()
0170     {
0171         mGroup = new SideBarGroup();
0172         q->setWidget(mGroup);
0173         EventWatcher::install(mGroup, QEvent::Show, q, SLOT(update()));
0174 
0175         auto container = new QWidget;
0176         setupUi(container);
0177         container->layout()->setContentsMargins(0, 0, 0, 0);
0178         mGroup->addWidget(container);
0179         mTagLayout = new FlowLayout;
0180         mTagLayout->setHorizontalSpacing(2);
0181         mTagLayout->setVerticalSpacing(2);
0182         mTagLayout->setContentsMargins(0, 0, 0, 0);
0183         mTagContainerWidget->setLayout(mTagLayout);
0184         DecoratedTag tempTag;
0185         tempTag.setVisible(false);
0186         mEditLabel = new QLabel(QStringLiteral("<a href='edit'>%1</a>").arg(i18n("Edit")));
0187         mEditLabel->setVisible(false);
0188         mEditLabel->setContentsMargins(tempTag.contentsMargins().left() / 2,
0189                                        tempTag.contentsMargins().top(),
0190                                        tempTag.contentsMargins().right() / 2,
0191                                        tempTag.contentsMargins().bottom());
0192         label_2->setContentsMargins(mEditLabel->contentsMargins());
0193 
0194         QObject::connect(mRatingWidget, SIGNAL(ratingChanged(int)), q, SLOT(slotRatingChanged(int)));
0195 
0196         mDescriptionTextEdit->installEventFilter(q);
0197 
0198         QObject::connect(mEditLabel, &QLabel::linkActivated, mEditTagsAction, &QAction::trigger);
0199     }
0200 
0201     void setupActions()
0202     {
0203         auto edit = new KActionCategory(i18nc("@title actions category", "Edit"), mActionCollection);
0204 
0205         mEditTagsAction = edit->addAction(QStringLiteral("edit_tags"));
0206         mEditTagsAction->setText(i18nc("@action", "Edit Tags"));
0207         mEditTagsAction->setIcon(QIcon::fromTheme(QStringLiteral("tag")));
0208         mActionCollection->setDefaultShortcut(mEditTagsAction, Qt::CTRL | Qt::Key_T);
0209         QObject::connect(mEditTagsAction, &QAction::triggered, q, &SemanticInfoContextManagerItem::showSemanticInfoDialog);
0210         mActions << mEditTagsAction;
0211 
0212         for (int rating = 0; rating <= 5; ++rating) {
0213             QAction *action = edit->addAction(QStringLiteral("rate_%1").arg(rating));
0214             if (rating == 0) {
0215                 action->setText(i18nc("@action Rating value of zero", "Zero"));
0216             } else {
0217                 action->setText(QString(rating, QChar(0x22C6))); /* 0x22C6 is the 'star' character */
0218             }
0219             mActionCollection->setDefaultShortcut(action, Qt::Key_0 + rating);
0220             QObject::connect(action, &QAction::triggered, q, [this, rating]() {
0221                 mRatingWidget->setRating(rating * 2);
0222             });
0223             mActions << action;
0224         }
0225     }
0226 
0227     void updateTags()
0228     {
0229         QLayoutItem *item;
0230         while ((item = mTagLayout->takeAt(0))) {
0231             auto tag = item->widget();
0232             if (tag != nullptr && tag != mEditLabel) {
0233                 tag->deleteLater();
0234             }
0235         }
0236         if (q->contextManager()->selectedFileItemList().isEmpty()) {
0237             mEditLabel->setVisible(false);
0238             return;
0239         }
0240 
0241         AbstractSemanticInfoBackEnd *backEnd = q->contextManager()->dirModel()->semanticInfoBackEnd();
0242 
0243         TagInfo::ConstIterator it = mTagInfo.constBegin(), end = mTagInfo.constEnd();
0244         QMap<QString, QString> labelMap;
0245         for (; it != end; ++it) {
0246             SemanticInfoTag tag = it.key();
0247             QString label = backEnd->labelForTag(tag);
0248             if (!it.value()) {
0249                 // Tag is not present for all urls
0250                 label += '*';
0251             }
0252             labelMap[label.toLower()] = label;
0253         }
0254         const QStringList labels(labelMap.values());
0255 
0256         for (const QString &label : labels) {
0257             auto decoratedTag = new DecoratedTag(label);
0258             mTagLayout->addWidget(decoratedTag);
0259         }
0260         mTagLayout->addWidget(mEditLabel);
0261         mEditLabel->setVisible(true);
0262         mTagLayout->update();
0263     }
0264 
0265     void updateSemanticInfoDialog()
0266     {
0267         mSemanticInfoDialog->mTagWidget->setEnabled(!q->contextManager()->selectedFileItemList().isEmpty());
0268         mSemanticInfoDialog->mTagWidget->setTagInfo(mTagInfo);
0269     }
0270 };
0271 
0272 SemanticInfoContextManagerItem::SemanticInfoContextManagerItem(ContextManager *manager, KActionCollection *actionCollection, ViewMainPage *viewMainPage)
0273     : AbstractContextManagerItem(manager)
0274     , d(new SemanticInfoContextManagerItemPrivate)
0275 {
0276     d->q = this;
0277     d->mActionCollection = actionCollection;
0278     d->mViewMainPage = viewMainPage;
0279 
0280     connect(contextManager(), &ContextManager::selectionChanged, this, &SemanticInfoContextManagerItem::slotSelectionChanged);
0281     connect(contextManager(), &ContextManager::selectionDataChanged, this, &SemanticInfoContextManagerItem::update);
0282     connect(contextManager(), &ContextManager::currentDirUrlChanged, this, &SemanticInfoContextManagerItem::update);
0283 
0284     d->setupActions();
0285     d->setupGroup();
0286 }
0287 
0288 SemanticInfoContextManagerItem::~SemanticInfoContextManagerItem()
0289 {
0290     delete d;
0291 }
0292 
0293 inline int ratingForVariant(const QVariant &variant)
0294 {
0295     if (variant.isValid()) {
0296         return variant.toInt();
0297     } else {
0298         return 0;
0299     }
0300 }
0301 
0302 void SemanticInfoContextManagerItem::slotSelectionChanged()
0303 {
0304     update();
0305 }
0306 
0307 void SemanticInfoContextManagerItem::update()
0308 {
0309     const KFileItemList itemList = contextManager()->selectedFileItemList();
0310 
0311     bool first = true;
0312     int rating = 0;
0313     QString description;
0314     SortedDirModel *dirModel = contextManager()->dirModel();
0315 
0316     // This hash stores for how many items the tag is present
0317     // If you have 3 items, and only 2 have the "Holiday" tag,
0318     // then tagHash["Holiday"] will be 2 at the end of the loop.
0319     using TagHash = QHash<QString, int>;
0320     TagHash tagHash;
0321 
0322     for (const KFileItem &item : itemList) {
0323         QModelIndex index = dirModel->indexForItem(item);
0324 
0325         QVariant value = dirModel->data(index, SemanticInfoDirModel::RatingRole);
0326         if (first) {
0327             rating = ratingForVariant(value);
0328         } else if (rating != ratingForVariant(value)) {
0329             // Ratings aren't the same, reset
0330             rating = 0;
0331         }
0332 
0333         QString indexDescription = index.data(SemanticInfoDirModel::DescriptionRole).toString();
0334         if (first) {
0335             description = indexDescription;
0336         } else if (description != indexDescription) {
0337             description.clear();
0338         }
0339 
0340         // Fill tagHash, incrementing the tag count if it's already there
0341         const TagSet tagSet = TagSet::fromVariant(index.data(SemanticInfoDirModel::TagsRole));
0342         for (const QString &tag : tagSet) {
0343             TagHash::Iterator it = tagHash.find(tag);
0344             if (it == tagHash.end()) {
0345                 tagHash[tag] = 1;
0346             } else {
0347                 ++it.value();
0348             }
0349         }
0350 
0351         first = false;
0352     }
0353     {
0354         SignalBlocker blocker(d->mRatingWidget);
0355         d->mRatingWidget->setRating(rating);
0356     }
0357     d->mDescriptionTextEdit->setText(description);
0358 
0359     // Init tagInfo from tagHash
0360     d->mTagInfo.clear();
0361     int itemCount = itemList.count();
0362     TagHash::ConstIterator it = tagHash.constBegin(), end = tagHash.constEnd();
0363     for (; it != end; ++it) {
0364         QString tag = it.key();
0365         int count = it.value();
0366         d->mTagInfo[tag] = count == itemCount;
0367     }
0368 
0369     bool enabled = !contextManager()->selectedFileItemList().isEmpty();
0370     for (QAction *action : qAsConst(d->mActions)) {
0371         action->setEnabled(enabled);
0372     }
0373     d->updateTags();
0374     if (d->mSemanticInfoDialog) {
0375         d->updateSemanticInfoDialog();
0376     }
0377 }
0378 
0379 void SemanticInfoContextManagerItem::slotRatingChanged(int rating)
0380 {
0381     const KFileItemList itemList = contextManager()->selectedFileItemList();
0382 
0383     // Show rating indicator in view mode, and only if sidebar is not visible
0384     if (d->mViewMainPage->isVisible() && !d->mRatingWidget->isVisible()) {
0385         if (!d->mRatingIndicator.data()) {
0386             d->mRatingIndicator = new RatingIndicator;
0387             d->mViewMainPage->showMessageWidget(d->mRatingIndicator, Qt::AlignBottom | Qt::AlignHCenter);
0388         }
0389         d->mRatingIndicator->setRating(rating);
0390     }
0391 
0392     SortedDirModel *dirModel = contextManager()->dirModel();
0393     for (const KFileItem &item : itemList) {
0394         QModelIndex index = dirModel->indexForItem(item);
0395         dirModel->setData(index, rating, SemanticInfoDirModel::RatingRole);
0396     }
0397 }
0398 
0399 void SemanticInfoContextManagerItem::storeDescription()
0400 {
0401     if (!d->mDescriptionTextEdit->document()->isModified()) {
0402         return;
0403     }
0404     d->mDescriptionTextEdit->document()->setModified(false);
0405     QString description = d->mDescriptionTextEdit->toPlainText();
0406     const KFileItemList itemList = contextManager()->selectedFileItemList();
0407 
0408     SortedDirModel *dirModel = contextManager()->dirModel();
0409     for (const KFileItem &item : itemList) {
0410         QModelIndex index = dirModel->indexForItem(item);
0411         dirModel->setData(index, description, SemanticInfoDirModel::DescriptionRole);
0412     }
0413 }
0414 
0415 void SemanticInfoContextManagerItem::assignTag(const SemanticInfoTag &tag)
0416 {
0417     const KFileItemList itemList = contextManager()->selectedFileItemList();
0418 
0419     SortedDirModel *dirModel = contextManager()->dirModel();
0420     for (const KFileItem &item : itemList) {
0421         QModelIndex index = dirModel->indexForItem(item);
0422         TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
0423         if (!tags.contains(tag)) {
0424             tags << tag;
0425             dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
0426         }
0427     }
0428 }
0429 
0430 void SemanticInfoContextManagerItem::removeTag(const SemanticInfoTag &tag)
0431 {
0432     const KFileItemList itemList = contextManager()->selectedFileItemList();
0433 
0434     SortedDirModel *dirModel = contextManager()->dirModel();
0435     for (const KFileItem &item : itemList) {
0436         QModelIndex index = dirModel->indexForItem(item);
0437         TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
0438         if (tags.contains(tag)) {
0439             tags.remove(tag);
0440             dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
0441         }
0442     }
0443 }
0444 
0445 void SemanticInfoContextManagerItem::showSemanticInfoDialog()
0446 {
0447     if (!d->mSemanticInfoDialog) {
0448         d->mSemanticInfoDialog = new SemanticInfoDialog(d->mGroup);
0449         d->mSemanticInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true);
0450 
0451         connect(d->mSemanticInfoDialog->mPreviousButton,
0452                 &QAbstractButton::clicked,
0453                 d->mActionCollection->action(QStringLiteral("go_previous")),
0454                 &QAction::trigger);
0455         connect(d->mSemanticInfoDialog->mNextButton, &QAbstractButton::clicked, d->mActionCollection->action(QStringLiteral("go_next")), &QAction::trigger);
0456         connect(d->mSemanticInfoDialog->mButtonBox, &QDialogButtonBox::rejected, d->mSemanticInfoDialog.data(), &QWidget::close);
0457 
0458         AbstractSemanticInfoBackEnd *backEnd = contextManager()->dirModel()->semanticInfoBackEnd();
0459         d->mSemanticInfoDialog->mTagWidget->setSemanticInfoBackEnd(backEnd);
0460         connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagAssigned, this, &SemanticInfoContextManagerItem::assignTag);
0461         connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagRemoved, this, &SemanticInfoContextManagerItem::removeTag);
0462     }
0463     d->updateSemanticInfoDialog();
0464     d->mSemanticInfoDialog->show();
0465 }
0466 
0467 bool SemanticInfoContextManagerItem::eventFilter(QObject *, QEvent *event)
0468 {
0469     if (event->type() == QEvent::FocusOut) {
0470         storeDescription();
0471     }
0472     return false;
0473 }
0474 
0475 } // namespace
0476 
0477 #include "moc_semanticinfocontextmanageritem.cpp"