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