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