1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (at your option) any later version.
10 
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
19 
20 */
21 // Self
22 #include "semanticinfocontextmanageritem.h"
23 
24 // Qt
25 #include <QAction>
26 #include <QDialog>
27 #include <QEvent>
28 #include <QPainter>
29 #include <QShortcut>
30 #include <QStyle>
31 #include <QTimer>
32 #include <QVBoxLayout>
33 
34 // KF
35 #include <KActionCategory>
36 #include <KActionCollection>
37 #include <KIconLoader>
38 #include <KLocalizedString>
39 #include <KRatingPainter>
40 #include <KSharedConfig>
41 #include <KWindowConfig>
42 
43 // Local
44 #include "sidebar.h"
45 #include "ui_semanticinfodialog.h"
46 #include "ui_semanticinfosidebaritem.h"
47 #include "viewmainpage.h"
48 #include <lib/contextmanager.h>
49 #include <lib/decoratedtag/decoratedtag.h>
50 #include <lib/documentview/documentview.h>
51 #include <lib/eventwatcher.h>
52 #include <lib/flowlayout.h>
53 #include <lib/hud/hudwidget.h>
54 #include <lib/semanticinfo/semanticinfodirmodel.h>
55 #include <lib/semanticinfo/sorteddirmodel.h>
56 #include <lib/signalblocker.h>
57 #include <lib/widgetfloater.h>
58 
59 namespace Gwenview
60 {
61 static const int RATING_INDICATOR_HIDE_DELAY = 2000;
62 
63 struct SemanticInfoDialog : public QDialog, public Ui_SemanticInfoDialog {
SemanticInfoDialogGwenview::SemanticInfoDialog64     SemanticInfoDialog(QWidget *parent)
65         : QDialog(parent)
66     {
67         setLayout(new QVBoxLayout);
68         auto *mainWidget = new QWidget;
69         layout()->addWidget(mainWidget);
70         setupUi(mainWidget);
71         mainWidget->layout()->setContentsMargins(0, 0, 0, 0);
72         setWindowTitle(mainWidget->windowTitle());
73 
74         KWindowConfig::restoreWindowSize(windowHandle(), configGroup());
75     }
76 
~SemanticInfoDialogGwenview::SemanticInfoDialog77     ~SemanticInfoDialog() override
78     {
79         KConfigGroup group = configGroup();
80         KWindowConfig::saveWindowSize(windowHandle(), group);
81     }
82 
configGroupGwenview::SemanticInfoDialog83     KConfigGroup configGroup() const
84     {
85         KSharedConfigPtr config = KSharedConfig::openConfig();
86         return KConfigGroup(config, "SemanticInfoDialog");
87     }
88 };
89 
90 /**
91  * A QGraphicsPixmapItem-like class, but which inherits from QGraphicsWidget
92  */
93 class GraphicsPixmapWidget : public QGraphicsWidget
94 {
95 public:
setPixmap(const QPixmap & pix)96     void setPixmap(const QPixmap &pix)
97     {
98         mPix = pix;
99         setMinimumSize(pix.size());
100     }
101 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)102     void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
103     {
104         painter->drawPixmap((size().width() - mPix.width()) / 2, (size().height() - mPix.height()) / 2, mPix);
105     }
106 
107 private:
108     QPixmap mPix;
109 };
110 
111 class RatingIndicator : public HudWidget
112 {
113 public:
RatingIndicator()114     RatingIndicator()
115         : HudWidget()
116         , mPixmapWidget(new GraphicsPixmapWidget)
117         , mDeleteTimer(new QTimer(this))
118     {
119         updatePixmap(0);
120         setOpacity(0);
121         init(mPixmapWidget, OptionNone);
122 
123         mDeleteTimer->setInterval(RATING_INDICATOR_HIDE_DELAY);
124         mDeleteTimer->setSingleShot(true);
125         connect(mDeleteTimer, &QTimer::timeout, this, &HudWidget::fadeOut);
126         connect(this, &HudWidget::fadedOut, this, &QObject::deleteLater);
127     }
128 
setRating(int rating)129     void setRating(int rating)
130     {
131         updatePixmap(rating);
132         update();
133         mDeleteTimer->start();
134         fadeIn();
135     }
136 
137 private:
138     GraphicsPixmapWidget *mPixmapWidget;
139     QTimer *mDeleteTimer;
140 
updatePixmap(int rating)141     void updatePixmap(int rating)
142     {
143         KRatingPainter ratingPainter;
144         const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Small);
145         QPixmap pix(iconSize * 5 + ratingPainter.spacing() * 4, iconSize);
146         pix.fill(Qt::transparent);
147         {
148             QPainter painter(&pix);
149             ratingPainter.paint(&painter, pix.rect(), rating);
150         }
151         mPixmapWidget->setPixmap(pix);
152     }
153 };
154 
155 struct SemanticInfoContextManagerItemPrivate : public Ui_SemanticInfoSideBarItem {
156     SemanticInfoContextManagerItem *q;
157     SideBarGroup *mGroup;
158     KActionCollection *mActionCollection;
159     ViewMainPage *mViewMainPage;
160     QPointer<SemanticInfoDialog> mSemanticInfoDialog;
161     TagInfo mTagInfo;
162     QAction *mEditTagsAction;
163     /** A list of all actions, so that we can disable them when necessary */
164     QList<QAction *> mActions;
165     QPointer<RatingIndicator> mRatingIndicator;
166     FlowLayout *mTagLayout;
167     QLabel *mEditLabel;
168 
setupGroupGwenview::SemanticInfoContextManagerItemPrivate169     void setupGroup()
170     {
171         mGroup = new SideBarGroup();
172         q->setWidget(mGroup);
173         EventWatcher::install(mGroup, QEvent::Show, q, SLOT(update()));
174 
175         auto *container = new QWidget;
176         setupUi(container);
177         container->layout()->setContentsMargins(0, 0, 0, 0);
178         mGroup->addWidget(container);
179         mTagLayout = new FlowLayout;
180         mTagLayout->setHorizontalSpacing(2);
181         mTagLayout->setVerticalSpacing(2);
182         mTagLayout->setContentsMargins(0, 0, 0, 0);
183         mTagContainerWidget->setLayout(mTagLayout);
184         DecoratedTag tempTag;
185         tempTag.setVisible(false);
186         mEditLabel = new QLabel(QStringLiteral("<a href='edit'>%1</a>").arg(i18n("Edit")));
187         mEditLabel->setVisible(false);
188         mEditLabel->setContentsMargins(tempTag.contentsMargins().left() / 2,
189                                        tempTag.contentsMargins().top(),
190                                        tempTag.contentsMargins().right() / 2,
191                                        tempTag.contentsMargins().bottom());
192         label_2->setContentsMargins(mEditLabel->contentsMargins());
193 
194         QObject::connect(mRatingWidget, SIGNAL(ratingChanged(int)), q, SLOT(slotRatingChanged(int)));
195 
196         mDescriptionTextEdit->installEventFilter(q);
197 
198         QObject::connect(mEditLabel, &QLabel::linkActivated, mEditTagsAction, &QAction::trigger);
199     }
200 
setupActionsGwenview::SemanticInfoContextManagerItemPrivate201     void setupActions()
202     {
203         auto *edit = new KActionCategory(i18nc("@title actions category", "Edit"), mActionCollection);
204 
205         mEditTagsAction = edit->addAction(QStringLiteral("edit_tags"));
206         mEditTagsAction->setText(i18nc("@action", "Edit Tags"));
207         mEditTagsAction->setIcon(QIcon::fromTheme(QStringLiteral("tag")));
208         mActionCollection->setDefaultShortcut(mEditTagsAction, Qt::CTRL | Qt::Key_T);
209         QObject::connect(mEditTagsAction, &QAction::triggered, q, &SemanticInfoContextManagerItem::showSemanticInfoDialog);
210         mActions << mEditTagsAction;
211 
212         for (int rating = 0; rating <= 5; ++rating) {
213             QAction *action = edit->addAction(QStringLiteral("rate_%1").arg(rating));
214             if (rating == 0) {
215                 action->setText(i18nc("@action Rating value of zero", "Zero"));
216             } else {
217                 action->setText(QString(rating, QChar(0x22C6))); /* 0x22C6 is the 'star' character */
218             }
219             mActionCollection->setDefaultShortcut(action, Qt::Key_0 + rating);
220             QObject::connect(action, &QAction::triggered, q, [this, rating]() {
221                 mRatingWidget->setRating(rating * 2);
222             });
223             mActions << action;
224         }
225     }
226 
updateTagsGwenview::SemanticInfoContextManagerItemPrivate227     void updateTags()
228     {
229         QLayoutItem *item;
230         while ((item = mTagLayout->takeAt(0))) {
231             auto tag = item->widget();
232             if (tag != nullptr && tag != mEditLabel) {
233                 tag->deleteLater();
234             }
235         }
236         if (q->contextManager()->selectedFileItemList().isEmpty()) {
237             mEditLabel->setVisible(false);
238             return;
239         }
240 
241         AbstractSemanticInfoBackEnd *backEnd = q->contextManager()->dirModel()->semanticInfoBackEnd();
242 
243         TagInfo::ConstIterator it = mTagInfo.constBegin(), end = mTagInfo.constEnd();
244         QMap<QString, QString> labelMap;
245         for (; it != end; ++it) {
246             SemanticInfoTag tag = it.key();
247             QString label = backEnd->labelForTag(tag);
248             if (!it.value()) {
249                 // Tag is not present for all urls
250                 label += '*';
251             }
252             labelMap[label.toLower()] = label;
253         }
254         const QStringList labels(labelMap.values());
255 
256         for (const QString &label : labels) {
257             DecoratedTag *decoratedTag = new DecoratedTag(label);
258             mTagLayout->addWidget(decoratedTag);
259         }
260         mTagLayout->addWidget(mEditLabel);
261         mEditLabel->setVisible(true);
262         mTagLayout->update();
263     }
264 
updateSemanticInfoDialogGwenview::SemanticInfoContextManagerItemPrivate265     void updateSemanticInfoDialog()
266     {
267         mSemanticInfoDialog->mTagWidget->setEnabled(!q->contextManager()->selectedFileItemList().isEmpty());
268         mSemanticInfoDialog->mTagWidget->setTagInfo(mTagInfo);
269     }
270 };
271 
SemanticInfoContextManagerItem(ContextManager * manager,KActionCollection * actionCollection,ViewMainPage * viewMainPage)272 SemanticInfoContextManagerItem::SemanticInfoContextManagerItem(ContextManager *manager, KActionCollection *actionCollection, ViewMainPage *viewMainPage)
273     : AbstractContextManagerItem(manager)
274     , d(new SemanticInfoContextManagerItemPrivate)
275 {
276     d->q = this;
277     d->mActionCollection = actionCollection;
278     d->mViewMainPage = viewMainPage;
279 
280     connect(contextManager(), &ContextManager::selectionChanged, this, &SemanticInfoContextManagerItem::slotSelectionChanged);
281     connect(contextManager(), &ContextManager::selectionDataChanged, this, &SemanticInfoContextManagerItem::update);
282     connect(contextManager(), &ContextManager::currentDirUrlChanged, this, &SemanticInfoContextManagerItem::update);
283 
284     d->setupActions();
285     d->setupGroup();
286 }
287 
~SemanticInfoContextManagerItem()288 SemanticInfoContextManagerItem::~SemanticInfoContextManagerItem()
289 {
290     delete d;
291 }
292 
ratingForVariant(const QVariant & variant)293 inline int ratingForVariant(const QVariant &variant)
294 {
295     if (variant.isValid()) {
296         return variant.toInt();
297     } else {
298         return 0;
299     }
300 }
301 
slotSelectionChanged()302 void SemanticInfoContextManagerItem::slotSelectionChanged()
303 {
304     update();
305 }
306 
update()307 void SemanticInfoContextManagerItem::update()
308 {
309     const KFileItemList itemList = contextManager()->selectedFileItemList();
310 
311     bool first = true;
312     int rating = 0;
313     QString description;
314     SortedDirModel *dirModel = contextManager()->dirModel();
315 
316     // This hash stores for how many items the tag is present
317     // If you have 3 items, and only 2 have the "Holiday" tag,
318     // then tagHash["Holiday"] will be 2 at the end of the loop.
319     using TagHash = QHash<QString, int>;
320     TagHash tagHash;
321 
322     for (const KFileItem &item : itemList) {
323         QModelIndex index = dirModel->indexForItem(item);
324 
325         QVariant value = dirModel->data(index, SemanticInfoDirModel::RatingRole);
326         if (first) {
327             rating = ratingForVariant(value);
328         } else if (rating != ratingForVariant(value)) {
329             // Ratings aren't the same, reset
330             rating = 0;
331         }
332 
333         QString indexDescription = index.data(SemanticInfoDirModel::DescriptionRole).toString();
334         if (first) {
335             description = indexDescription;
336         } else if (description != indexDescription) {
337             description.clear();
338         }
339 
340         // Fill tagHash, incrementing the tag count if it's already there
341         const TagSet tagSet = TagSet::fromVariant(index.data(SemanticInfoDirModel::TagsRole));
342         for (const QString &tag : tagSet) {
343             TagHash::Iterator it = tagHash.find(tag);
344             if (it == tagHash.end()) {
345                 tagHash[tag] = 1;
346             } else {
347                 ++it.value();
348             }
349         }
350 
351         first = false;
352     }
353     {
354         SignalBlocker blocker(d->mRatingWidget);
355         d->mRatingWidget->setRating(rating);
356     }
357     d->mDescriptionTextEdit->setText(description);
358 
359     // Init tagInfo from tagHash
360     d->mTagInfo.clear();
361     int itemCount = itemList.count();
362     TagHash::ConstIterator it = tagHash.constBegin(), end = tagHash.constEnd();
363     for (; it != end; ++it) {
364         QString tag = it.key();
365         int count = it.value();
366         d->mTagInfo[tag] = count == itemCount;
367     }
368 
369     bool enabled = !contextManager()->selectedFileItemList().isEmpty();
370     for (QAction *action : qAsConst(d->mActions)) {
371         action->setEnabled(enabled);
372     }
373     d->updateTags();
374     if (d->mSemanticInfoDialog) {
375         d->updateSemanticInfoDialog();
376     }
377 }
378 
slotRatingChanged(int rating)379 void SemanticInfoContextManagerItem::slotRatingChanged(int rating)
380 {
381     const KFileItemList itemList = contextManager()->selectedFileItemList();
382 
383     // Show rating indicator in view mode, and only if sidebar is not visible
384     if (d->mViewMainPage->isVisible() && !d->mRatingWidget->isVisible()) {
385         if (!d->mRatingIndicator.data()) {
386             d->mRatingIndicator = new RatingIndicator;
387             d->mViewMainPage->showMessageWidget(d->mRatingIndicator, Qt::AlignBottom | Qt::AlignHCenter);
388         }
389         d->mRatingIndicator->setRating(rating);
390     }
391 
392     SortedDirModel *dirModel = contextManager()->dirModel();
393     for (const KFileItem &item : itemList) {
394         QModelIndex index = dirModel->indexForItem(item);
395         dirModel->setData(index, rating, SemanticInfoDirModel::RatingRole);
396     }
397 }
398 
storeDescription()399 void SemanticInfoContextManagerItem::storeDescription()
400 {
401     if (!d->mDescriptionTextEdit->document()->isModified()) {
402         return;
403     }
404     d->mDescriptionTextEdit->document()->setModified(false);
405     QString description = d->mDescriptionTextEdit->toPlainText();
406     const KFileItemList itemList = contextManager()->selectedFileItemList();
407 
408     SortedDirModel *dirModel = contextManager()->dirModel();
409     for (const KFileItem &item : itemList) {
410         QModelIndex index = dirModel->indexForItem(item);
411         dirModel->setData(index, description, SemanticInfoDirModel::DescriptionRole);
412     }
413 }
414 
assignTag(const SemanticInfoTag & tag)415 void SemanticInfoContextManagerItem::assignTag(const SemanticInfoTag &tag)
416 {
417     const KFileItemList itemList = contextManager()->selectedFileItemList();
418 
419     SortedDirModel *dirModel = contextManager()->dirModel();
420     for (const KFileItem &item : itemList) {
421         QModelIndex index = dirModel->indexForItem(item);
422         TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
423         if (!tags.contains(tag)) {
424             tags << tag;
425             dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
426         }
427     }
428 }
429 
removeTag(const SemanticInfoTag & tag)430 void SemanticInfoContextManagerItem::removeTag(const SemanticInfoTag &tag)
431 {
432     const KFileItemList itemList = contextManager()->selectedFileItemList();
433 
434     SortedDirModel *dirModel = contextManager()->dirModel();
435     for (const KFileItem &item : itemList) {
436         QModelIndex index = dirModel->indexForItem(item);
437         TagSet tags = TagSet::fromVariant(dirModel->data(index, SemanticInfoDirModel::TagsRole));
438         if (tags.contains(tag)) {
439             tags.remove(tag);
440             dirModel->setData(index, tags.toVariant(), SemanticInfoDirModel::TagsRole);
441         }
442     }
443 }
444 
showSemanticInfoDialog()445 void SemanticInfoContextManagerItem::showSemanticInfoDialog()
446 {
447     if (!d->mSemanticInfoDialog) {
448         d->mSemanticInfoDialog = new SemanticInfoDialog(d->mGroup);
449         d->mSemanticInfoDialog->setAttribute(Qt::WA_DeleteOnClose, true);
450 
451         connect(d->mSemanticInfoDialog->mPreviousButton,
452                 &QAbstractButton::clicked,
453                 d->mActionCollection->action(QStringLiteral("go_previous")),
454                 &QAction::trigger);
455         connect(d->mSemanticInfoDialog->mNextButton, &QAbstractButton::clicked, d->mActionCollection->action(QStringLiteral("go_next")), &QAction::trigger);
456         connect(d->mSemanticInfoDialog->mButtonBox, &QDialogButtonBox::rejected, d->mSemanticInfoDialog.data(), &QWidget::close);
457 
458         AbstractSemanticInfoBackEnd *backEnd = contextManager()->dirModel()->semanticInfoBackEnd();
459         d->mSemanticInfoDialog->mTagWidget->setSemanticInfoBackEnd(backEnd);
460         connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagAssigned, this, &SemanticInfoContextManagerItem::assignTag);
461         connect(d->mSemanticInfoDialog->mTagWidget, &TagWidget::tagRemoved, this, &SemanticInfoContextManagerItem::removeTag);
462     }
463     d->updateSemanticInfoDialog();
464     d->mSemanticInfoDialog->show();
465 }
466 
eventFilter(QObject *,QEvent * event)467 bool SemanticInfoContextManagerItem::eventFilter(QObject *, QEvent *event)
468 {
469     if (event->type() == QEvent::FocusOut) {
470         storeDescription();
471     }
472     return false;
473 }
474 
475 } // namespace
476