1 /*
2 * Copyright (C) 2013-2015 Ofer Kashayov <oferkv@live.com>
3 * This file is part of Phototonic Image Viewer.
4 *
5 * Phototonic is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * Phototonic is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with Phototonic. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "Tags.h"
20 #include "Settings.h"
21 #include "ProgressDialog.h"
22 #include "MessageBox.h"
23
ImageTags(QWidget * parent,ThumbsViewer * thumbsViewer,MetadataCache * metadataCache)24 ImageTags::ImageTags(QWidget *parent, ThumbsViewer *thumbsViewer, MetadataCache *metadataCache) : QWidget(parent) {
25 tagsTree = new QTreeWidget;
26 tagsTree->setColumnCount(2);
27 tagsTree->setDragEnabled(false);
28 tagsTree->setSortingEnabled(true);
29 tagsTree->header()->close();
30 tagsTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
31 this->thumbView = thumbsViewer;
32 this->metadataCache = metadataCache;
33 negateFilterEnabled = false;
34
35 tabs = new QTabBar(this);
36 tabs->addTab(tr("Selection"));
37 tabs->addTab(tr("Filter"));
38 tabs->setTabIcon(0, QIcon(":/images/tag_yellow.png"));
39 tabs->setTabIcon(1, QIcon(":/images/tag_filter_off.png"));
40 tabs->setExpanding(false);
41 connect(tabs, SIGNAL(currentChanged(int)), this, SLOT(tabsChanged(int)));
42
43 QVBoxLayout *mainLayout = new QVBoxLayout;
44 mainLayout->setContentsMargins(0, 3, 0, 0);
45 mainLayout->setSpacing(0);
46 mainLayout->addWidget(tabs);
47 mainLayout->addWidget(tagsTree);
48 setLayout(mainLayout);
49 currentDisplayMode = SelectionTagsDisplay;
50 dirFilteringActive = false;
51
52 connect(tagsTree, SIGNAL(itemChanged(QTreeWidgetItem * , int)),
53 this, SLOT(saveLastChangedTag(QTreeWidgetItem * , int)));
54 connect(tagsTree, SIGNAL(itemClicked(QTreeWidgetItem * , int)),
55 this, SLOT(tagClicked(QTreeWidgetItem * , int)));
56
57 tagsTree->setContextMenuPolicy(Qt::CustomContextMenu);
58 connect(tagsTree, SIGNAL(customContextMenuRequested(QPoint)), SLOT(showMenu(QPoint)));
59
60 addToSelectionAction = new QAction(tr("Tag"), this);
61 addToSelectionAction->setIcon(QIcon(":/images/tag_yellow.png"));
62 connect(addToSelectionAction, SIGNAL(triggered()), this, SLOT(addTagsToSelection()));
63
64 removeFromSelectionAction = new QAction(tr("Untag"), this);
65 connect(removeFromSelectionAction, SIGNAL(triggered()), this, SLOT(removeTagsFromSelection()));
66
67 actionAddTag = new QAction(tr("New Tag"), this);
68 actionAddTag->setIcon(QIcon(":/images/new_tag.png"));
69 connect(actionAddTag, SIGNAL(triggered()), this, SLOT(addNewTag()));
70
71 removeTagAction = new QAction(tr("Delete Tag"), this);
72 removeTagAction->setIcon(QIcon::fromTheme("edit-delete", QIcon(":/images/delete.png")));
73
74 actionClearTagsFilter = new QAction(tr("Clear Filters"), this);
75 actionClearTagsFilter->setIcon(QIcon(":/images/tag_filter_off.png"));
76 connect(actionClearTagsFilter, SIGNAL(triggered()), this, SLOT(clearTagFilters()));
77
78 negateAction = new QAction(tr("Negate"), this);
79 negateAction->setCheckable(true);
80 connect(negateAction, SIGNAL(triggered()), this, SLOT(negateFilter()));
81
82 tagsMenu = new QMenu("");
83 tagsMenu->addAction(addToSelectionAction);
84 tagsMenu->addAction(removeFromSelectionAction);
85 tagsMenu->addSeparator();
86 tagsMenu->addAction(actionAddTag);
87 tagsMenu->addAction(removeTagAction);
88 tagsMenu->addSeparator();
89 tagsMenu->addAction(actionClearTagsFilter);
90 tagsMenu->addAction(negateAction);
91 }
92
redrawTagTree()93 void ImageTags::redrawTagTree() {
94 tagsTree->resizeColumnToContents(0);
95 tagsTree->sortItems(0, Qt::AscendingOrder);
96 }
97
showMenu(QPoint point)98 void ImageTags::showMenu(QPoint point) {
99 QTreeWidgetItem *item = tagsTree->itemAt(point);
100 addToSelectionAction->setEnabled(item != NULL);
101 removeFromSelectionAction->setEnabled(item != NULL);
102 removeTagAction->setEnabled(item != NULL);
103 tagsMenu->popup(tagsTree->viewport()->mapToGlobal(point));
104 }
105
setTagIcon(QTreeWidgetItem * tagItem,TagIcons icon)106 void ImageTags::setTagIcon(QTreeWidgetItem *tagItem, TagIcons icon) {
107 switch (icon) {
108 case TagIconDisabled:
109 tagItem->setIcon(0, QIcon(":/images/tag_grey.png"));
110 break;
111 case TagIconEnabled:
112 tagItem->setIcon(0, QIcon(":/images/tag_yellow.png"));
113 break;
114 case TagIconMultiple:
115 tagItem->setIcon(0, QIcon(":/images/tag_multi.png"));
116 break;
117 case TagIconFilterEnabled:
118 tagItem->setIcon(0, QIcon(":/images/tag_filter_on.png"));
119 break;
120 case TagIconFilterDisabled:
121 tagItem->setIcon(0, QIcon(":/images/tag_filter_off.png"));
122 break;
123 case TagIconFilterNegate:
124 tagItem->setIcon(0, QIcon(":/images/tag_filter_negate.png"));
125 break;
126 }
127 }
128
addTag(QString tagName,bool tagChecked)129 void ImageTags::addTag(QString tagName, bool tagChecked) {
130 QTreeWidgetItem *tagItem = new QTreeWidgetItem();
131 tagItem->setText(0, tagName);
132 tagItem->setCheckState(0, tagChecked ? Qt::Checked : Qt::Unchecked);
133 setTagIcon(tagItem, tagChecked ? TagIconEnabled : TagIconDisabled);
134 tagsTree->addTopLevelItem(tagItem);
135 }
136
writeTagsToImage(QString & imageFileName,QSet<QString> & newTags)137 bool ImageTags::writeTagsToImage(QString &imageFileName, QSet<QString> &newTags) {
138 QSet<QString> imageTags;
139 Exiv2::Image::AutoPtr exifImage;
140
141 try {
142 exifImage = Exiv2::ImageFactory::open(imageFileName.toStdString());
143 exifImage->readMetadata();
144
145 Exiv2::IptcData newIptcData;
146
147 /* copy existing data */
148 Exiv2::IptcData &iptcData = exifImage->iptcData();
149 if (!iptcData.empty()) {
150 QString key;
151 Exiv2::IptcData::iterator end = iptcData.end();
152 for (Exiv2::IptcData::iterator iptcIt = iptcData.begin(); iptcIt != end; ++iptcIt) {
153 if (iptcIt->tagName() != "Keywords") {
154 newIptcData.add(*iptcIt);
155 }
156 }
157 }
158
159 /* add new tags */
160 QSetIterator<QString> newTagsIt(newTags);
161 while (newTagsIt.hasNext()) {
162 QString tag = newTagsIt.next();
163 Exiv2::Value::AutoPtr value = Exiv2::Value::create(Exiv2::string);
164 value->read(tag.toStdString());
165 Exiv2::IptcKey key("Iptc.Application2.Keywords");
166 newIptcData.add(key, value.get());
167 }
168
169 exifImage->setIptcData(newIptcData);
170 exifImage->writeMetadata();
171 }
172 catch (Exiv2::Error &error) {
173 MessageBox msgBox(this);
174 msgBox.critical(tr("Error"), tr("Failed to save tags to ") + imageFileName);
175 return false;
176 }
177
178 return true;
179 }
180
showSelectedImagesTags()181 void ImageTags::showSelectedImagesTags() {
182 static bool busy = false;
183 if (busy)
184 return;
185 busy = true;
186 QStringList selectedThumbs = thumbView->getSelectedThumbsList();
187
188 setActiveViewMode(SelectionTagsDisplay);
189
190 int selectedThumbsNum = selectedThumbs.size();
191 QMap<QString, int> tagsCount;
192 for (int i = 0; i < selectedThumbsNum; ++i) {
193 QSetIterator<QString> imageTagsIter(metadataCache->getImageTags(selectedThumbs[i]));
194 while (imageTagsIter.hasNext()) {
195 QString imageTag = imageTagsIter.next();
196 tagsCount[imageTag]++;
197
198 if (!Settings::knownTags.contains(imageTag)) {
199 addTag(imageTag, true);
200 Settings::knownTags.insert(imageTag);
201 }
202 }
203 }
204
205 bool imagesTagged = false, imagesTaggedMixed = false;
206 QTreeWidgetItemIterator it(tagsTree);
207 while (*it) {
208 QString tagName = (*it)->text(0);
209 int tagCountTotal = tagsCount[tagName];
210
211 if (selectedThumbsNum == 0) {
212 (*it)->setCheckState(0, Qt::Unchecked);
213 (*it)->setFlags((*it)->flags() & ~Qt::ItemIsUserCheckable);
214 setTagIcon(*it, TagIconDisabled);
215 } else if (tagCountTotal == selectedThumbsNum) {
216 (*it)->setCheckState(0, Qt::Checked);
217 (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
218 setTagIcon(*it, TagIconEnabled);
219 imagesTagged = true;
220 } else if (tagCountTotal) {
221 (*it)->setCheckState(0, Qt::PartiallyChecked);
222 (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
223 setTagIcon(*it, TagIconMultiple);
224 imagesTaggedMixed = true;
225 } else {
226 (*it)->setCheckState(0, Qt::Unchecked);
227 (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
228 setTagIcon(*it, TagIconDisabled);
229 }
230 ++it;
231 }
232
233 if (imagesTagged) {
234 tabs->setTabIcon(0, QIcon(":/images/tag_yellow.png"));
235 } else if (imagesTaggedMixed) {
236 tabs->setTabIcon(0, QIcon(":/images/tag_multi.png"));
237 } else {
238 tabs->setTabIcon(0, QIcon(":/images/tag_grey.png"));
239 }
240
241 addToSelectionAction->setEnabled(selectedThumbsNum ? true : false);
242 removeFromSelectionAction->setEnabled(selectedThumbsNum ? true : false);
243
244 redrawTagTree();
245 busy = false;
246 }
247
showTagsFilter()248 void ImageTags::showTagsFilter() {
249 static bool busy = false;
250 if (busy)
251 return;
252 busy = true;
253
254 setActiveViewMode(DirectoryTagsDisplay);
255
256 QTreeWidgetItemIterator it(tagsTree);
257 while (*it) {
258 QString tagName = (*it)->text(0);
259
260 (*it)->setFlags((*it)->flags() | Qt::ItemIsUserCheckable);
261 if (imageFilteringTags.contains(tagName)) {
262 (*it)->setCheckState(0, Qt::Checked);
263 setTagIcon(*it, negateFilterEnabled ? TagIconFilterNegate : TagIconFilterEnabled);
264 } else {
265 (*it)->setCheckState(0, Qt::Unchecked);
266 setTagIcon(*it, TagIconFilterDisabled);
267 }
268 ++it;
269 }
270
271 redrawTagTree();
272 busy = false;
273 }
274
populateTagsTree()275 void ImageTags::populateTagsTree() {
276 tagsTree->clear();
277 QSetIterator<QString> knownTagsIt(Settings::knownTags);
278 while (knownTagsIt.hasNext()) {
279 QString tag = knownTagsIt.next();
280 addTag(tag, false);
281 }
282
283 redrawTagTree();
284
285 if (currentDisplayMode == SelectionTagsDisplay) {
286 showSelectedImagesTags();
287 } else {
288 showTagsFilter();
289 }
290 }
291
setActiveViewMode(TagsDisplayMode mode)292 void ImageTags::setActiveViewMode(TagsDisplayMode mode) {
293 currentDisplayMode = mode;
294 actionAddTag->setVisible(currentDisplayMode == SelectionTagsDisplay);
295 removeTagAction->setVisible(currentDisplayMode == SelectionTagsDisplay);
296 addToSelectionAction->setVisible(currentDisplayMode == SelectionTagsDisplay);
297 removeFromSelectionAction->setVisible(currentDisplayMode == SelectionTagsDisplay);
298 actionClearTagsFilter->setVisible(currentDisplayMode == DirectoryTagsDisplay);
299 negateAction->setVisible(currentDisplayMode == DirectoryTagsDisplay);
300 }
301
isImageFilteredOut(QString imageFileName)302 bool ImageTags::isImageFilteredOut(QString imageFileName) {
303 QSet<QString> imageTags = metadataCache->getImageTags(imageFileName);
304
305 QSetIterator<QString> filteredTagsIt(imageFilteringTags);
306 while (filteredTagsIt.hasNext()) {
307 if (imageTags.contains(filteredTagsIt.next())) {
308 return negateFilterEnabled;
309 }
310 }
311
312 return !negateFilterEnabled;
313 }
314
resetTagsState()315 void ImageTags::resetTagsState() {
316 tagsTree->clear();
317 metadataCache->clear();
318 }
319
getCheckedTags(Qt::CheckState tagState)320 QSet<QString> ImageTags::getCheckedTags(Qt::CheckState tagState) {
321 QSet<QString> checkedTags;
322 QTreeWidgetItemIterator it(tagsTree);
323
324 while (*it) {
325 if ((*it)->checkState(0) == tagState) {
326 checkedTags.insert((*it)->text(0));
327 }
328 ++it;
329 }
330
331 return checkedTags;
332 }
333
applyTagFiltering()334 void ImageTags::applyTagFiltering() {
335 imageFilteringTags = getCheckedTags(Qt::Checked);
336 if (imageFilteringTags.size()) {
337 dirFilteringActive = true;
338 if (negateFilterEnabled) {
339 tabs->setTabIcon(1, QIcon(":/images/tag_filter_negate.png"));
340 } else {
341 tabs->setTabIcon(1, QIcon(":/images/tag_filter_on.png"));
342 }
343 } else {
344 dirFilteringActive = false;
345 tabs->setTabIcon(1, QIcon(":/images/tag_filter_off.png"));
346 }
347
348 emit reloadThumbs();
349 }
350
applyUserAction(QTreeWidgetItem * item)351 void ImageTags::applyUserAction(QTreeWidgetItem *item) {
352 QList<QTreeWidgetItem *> tagsList;
353 tagsList << item;
354 applyUserAction(tagsList);
355 }
356
applyUserAction(QList<QTreeWidgetItem * > tagsList)357 void ImageTags::applyUserAction(QList<QTreeWidgetItem *> tagsList) {
358 int processEventsCounter = 0;
359 ProgressDialog *progressDialog = new ProgressDialog(this);
360 progressDialog->show();
361
362 QStringList currentSelectedImages = thumbView->getSelectedThumbsList();
363 for (int currentImage = 0; currentImage < currentSelectedImages.size(); ++currentImage) {
364
365 QString imageName = currentSelectedImages[currentImage];
366 for (int i = tagsList.size() - 1; i > -1; --i) {
367 Qt::CheckState tagState = tagsList.at(i)->checkState(0);
368 setTagIcon(tagsList.at(i), (tagState == Qt::Checked ? TagIconEnabled : TagIconDisabled));
369 QString tagName = tagsList.at(i)->text(0);
370
371 if (tagState == Qt::Checked) {
372 progressDialog->opLabel->setText(tr("Tagging ") + imageName);
373 metadataCache->addTagToImage(imageName, tagName);
374 } else {
375 progressDialog->opLabel->setText(tr("Untagging ") + imageName);
376 metadataCache->removeTagFromImage(imageName, tagName);
377 }
378 }
379
380 if (!writeTagsToImage(imageName, metadataCache->getImageTags(imageName))) {
381 metadataCache->removeImage(imageName);
382 }
383
384 ++processEventsCounter;
385 if (processEventsCounter > 9) {
386 processEventsCounter = 0;
387 QApplication::processEvents();
388 }
389
390 if (progressDialog->abortOp) {
391 break;
392 }
393 }
394
395 progressDialog->close();
396 delete (progressDialog);
397 }
398
saveLastChangedTag(QTreeWidgetItem * item,int)399 void ImageTags::saveLastChangedTag(QTreeWidgetItem *item, int) {
400 lastChangedTagItem = item;
401 }
402
tabsChanged(int index)403 void ImageTags::tabsChanged(int index) {
404 if (!index) {
405 showSelectedImagesTags();
406 } else {
407 showTagsFilter();
408 }
409 }
410
tagClicked(QTreeWidgetItem * item,int)411 void ImageTags::tagClicked(QTreeWidgetItem *item, int) {
412 if (item == lastChangedTagItem) {
413 if (currentDisplayMode == DirectoryTagsDisplay) {
414 applyTagFiltering();
415 } else {
416 applyUserAction(item);
417 }
418 lastChangedTagItem = 0;
419 }
420 }
421
removeTagsFromSelection()422 void ImageTags::removeTagsFromSelection() {
423 for (int i = tagsTree->selectedItems().size() - 1; i > -1; --i) {
424 tagsTree->selectedItems().at(i)->setCheckState(0, Qt::Unchecked);
425 }
426
427 applyUserAction(tagsTree->selectedItems());
428 }
429
addTagsToSelection()430 void ImageTags::addTagsToSelection() {
431 for (int i = tagsTree->selectedItems().size() - 1; i > -1; --i) {
432 tagsTree->selectedItems().at(i)->setCheckState(0, Qt::Checked);
433 }
434
435 applyUserAction(tagsTree->selectedItems());
436 }
437
clearTagFilters()438 void ImageTags::clearTagFilters() {
439 QTreeWidgetItemIterator it(tagsTree);
440 while (*it) {
441 (*it)->setCheckState(0, Qt::Unchecked);
442 ++it;
443 }
444
445 imageFilteringTags.clear();
446 applyTagFiltering();
447 }
448
negateFilter()449 void ImageTags::negateFilter() {
450 negateFilterEnabled = negateAction->isChecked();
451 applyTagFiltering();
452 }
453
addNewTag()454 void ImageTags::addNewTag() {
455 bool ok;
456 QString title = tr("Add a new tag");
457 QString newTagName = QInputDialog::getText(this, title, tr("Enter new tag name"),
458 QLineEdit::Normal, "", &ok);
459 if (!ok) {
460 return;
461 }
462
463 if (newTagName.isEmpty()) {
464 MessageBox msgBox(this);
465 msgBox.critical(tr("Error"), tr("No name entered"));
466 return;
467 }
468
469 QSetIterator<QString> knownTagsIt(Settings::knownTags);
470 while (knownTagsIt.hasNext()) {
471 QString tag = knownTagsIt.next();
472 if (newTagName == tag) {
473 MessageBox msgBox(this);
474 msgBox.critical(tr("Error"), tr("Tag ") + newTagName + tr(" already exists"));
475 return;
476 }
477 }
478
479 addTag(newTagName, false);
480 Settings::knownTags.insert(newTagName);
481 redrawTagTree();
482 }
483
removeTag()484 void ImageTags::removeTag() {
485 if (!tagsTree->selectedItems().size()) {
486 return;
487 }
488
489 MessageBox msgBox(this);
490 msgBox.setText(tr("Delete selected tags(s)?"));
491 msgBox.setWindowTitle(tr("Delete tag"));
492 msgBox.setIcon(MessageBox::Warning);
493 msgBox.setStandardButtons(MessageBox::Yes | MessageBox::Cancel);
494 msgBox.setDefaultButton(MessageBox::Cancel);
495 msgBox.setButtonText(MessageBox::Yes, tr("Yes"));
496 msgBox.setButtonText(MessageBox::Cancel, tr("Cancel"));
497
498 if (msgBox.exec() != MessageBox::Yes) {
499 return;
500 }
501
502 bool removedTagWasChecked = false;
503 for (int i = tagsTree->selectedItems().size() - 1; i > -1; --i) {
504
505 QString tagName = tagsTree->selectedItems().at(i)->text(0);
506 Settings::knownTags.remove(tagName);
507
508 if (imageFilteringTags.contains(tagName)) {
509 imageFilteringTags.remove(tagName);
510 removedTagWasChecked = true;
511 }
512
513 tagsTree->takeTopLevelItem(tagsTree->indexOfTopLevelItem(tagsTree->selectedItems().at(i)));
514 }
515
516 if (removedTagWasChecked) {
517 applyTagFiltering();
518 }
519 }
520
521