1 /*
2  *  SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
3  *  SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  */
7 
8 #include "dolphinfacetswidget.h"
9 
10 #include <KLocalizedString>
11 #include <KProtocolInfo>
12 
13 #include <QComboBox>
14 #include <QDate>
15 #include <QEvent>
16 #include <QHBoxLayout>
17 #include <QIcon>
18 #include <QMenu>
19 #include <QToolButton>
20 
DolphinFacetsWidget(QWidget * parent)21 DolphinFacetsWidget::DolphinFacetsWidget(QWidget* parent) :
22     QWidget(parent),
23     m_typeSelector(nullptr),
24     m_dateSelector(nullptr),
25     m_ratingSelector(nullptr),
26     m_tagsSelector(nullptr)
27 {
28     m_typeSelector = new QComboBox(this);
29     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("none")), i18nc("@item:inlistbox", "Any Type"), QString());
30     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("inode-directory")), i18nc("@item:inlistbox", "Folders") , QStringLiteral("Folder"));
31     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), i18nc("@item:inlistbox", "Documents") , QStringLiteral("Document"));
32     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("image-x-generic")), i18nc("@item:inlistbox", "Images") , QStringLiteral("Image"));
33     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("audio-x-generic")), i18nc("@item:inlistbox", "Audio Files"), QStringLiteral("Audio"));
34     m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18nc("@item:inlistbox", "Videos") , QStringLiteral("Video"));
35     initComboBox(m_typeSelector);
36 
37     const QDate currentDate = QDate::currentDate();
38 
39     m_dateSelector = new QComboBox(this);
40     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar")), i18nc("@item:inlistbox", "Any Date"), QDate());
41     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Today") , currentDate);
42     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Yesterday") , currentDate.addDays(-1));
43     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-week")), i18nc("@item:inlistbox", "This Week") , currentDate.addDays(1 - currentDate.dayOfWeek()));
44     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-month")), i18nc("@item:inlistbox", "This Month"), currentDate.addDays(1 - currentDate.day()));
45     m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-year")), i18nc("@item:inlistbox", "This Year") , currentDate.addDays(1 - currentDate.dayOfYear()));
46     initComboBox(m_dateSelector);
47 
48     m_ratingSelector = new QComboBox(this);
49     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("non-starred-symbolic")), i18nc("@item:inlistbox", "Any Rating"), 0);
50     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 1);
51     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 2);
52     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 3);
53     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "4 or more"), 4);
54     m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "Highest Rating"), 5);
55     initComboBox(m_ratingSelector);
56 
57     m_clearTagsAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-all")), i18nc("@action:inmenu", "Clear Selection"), this);
58     connect(m_clearTagsAction, &QAction::triggered, this, [this]() {
59         resetSearchTags();
60         Q_EMIT facetChanged();
61     });
62 
63     m_tagsSelector = new QToolButton(this);
64     m_tagsSelector->setIcon(QIcon::fromTheme(QStringLiteral("tag")));
65     m_tagsSelector->setMenu(new QMenu(this));
66     m_tagsSelector->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
67     m_tagsSelector->setPopupMode(QToolButton::MenuButtonPopup);
68     m_tagsSelector->setAutoRaise(true);
69     updateTagsSelector();
70 
71     connect(m_tagsSelector, &QToolButton::clicked, m_tagsSelector, &QToolButton::showMenu);
72     connect(m_tagsSelector->menu(), &QMenu::aboutToShow, this, &DolphinFacetsWidget::updateTagsMenu);
73     connect(&m_tagsLister, &KCoreDirLister::itemsAdded, this, &DolphinFacetsWidget::updateTagsMenuItems);
74     updateTagsMenu();
75 
76     QHBoxLayout* topLayout = new QHBoxLayout(this);
77     topLayout->setContentsMargins(0, 0, 0, 0);
78     topLayout->addWidget(m_typeSelector);
79     topLayout->addWidget(m_dateSelector);
80     topLayout->addWidget(m_ratingSelector);
81     topLayout->addWidget(m_tagsSelector);
82 
83     resetSearchTerms();
84 }
85 
~DolphinFacetsWidget()86 DolphinFacetsWidget::~DolphinFacetsWidget()
87 {
88 }
89 
changeEvent(QEvent * event)90 void DolphinFacetsWidget::changeEvent(QEvent *event)
91 {
92     if (event->type() == QEvent::EnabledChange) {
93         if (isEnabled()) {
94             updateTagsSelector();
95         } else {
96             resetSearchTerms();
97         }
98     }
99 }
100 
resetSearchTerms()101 void DolphinFacetsWidget::resetSearchTerms()
102 {
103     m_typeSelector->setCurrentIndex(0);
104     m_dateSelector->setCurrentIndex(0);
105     m_ratingSelector->setCurrentIndex(0);
106 
107     resetSearchTags();
108 }
109 
searchTerms() const110 QStringList DolphinFacetsWidget::searchTerms() const
111 {
112     QStringList terms;
113 
114     if (m_ratingSelector->currentIndex() > 0) {
115         const int rating = m_ratingSelector->currentData().toInt() * 2;
116         terms << QStringLiteral("rating>=%1").arg(rating);
117     }
118 
119     if (m_dateSelector->currentIndex() > 0) {
120         const QDate date = m_dateSelector->currentData().toDate();
121         terms << QStringLiteral("modified>=%1").arg(date.toString(Qt::ISODate));
122     }
123 
124     if (!m_searchTags.isEmpty()) {
125         for (auto const &tag : m_searchTags) {
126             if (tag.contains(QLatin1Char(' '))) {
127                 terms << QStringLiteral("tag:\"%1\"").arg(tag);
128             } else {
129                 terms << QStringLiteral("tag:%1").arg(tag);
130             }
131         }
132     }
133 
134     return terms;
135 }
136 
facetType() const137 QString DolphinFacetsWidget::facetType() const
138 {
139     return m_typeSelector->currentData().toString();
140 }
141 
isSearchTerm(const QString & term) const142 bool DolphinFacetsWidget::isSearchTerm(const QString& term) const
143 {
144     static const QLatin1String searchTokens[] {
145         QLatin1String("modified>="),
146         QLatin1String("rating>="),
147         QLatin1String("tag:"), QLatin1String("tag=")
148     };
149 
150     for (const auto &searchToken : searchTokens) {
151         if (term.startsWith(searchToken)) {
152             return true;
153         }
154     }
155     return false;
156 }
157 
setSearchTerm(const QString & term)158 void DolphinFacetsWidget::setSearchTerm(const QString& term)
159 {
160     if (term.startsWith(QLatin1String("modified>="))) {
161         const QString value = term.mid(10);
162         const QDate date = QDate::fromString(value, Qt::ISODate);
163         setTimespan(date);
164     } else if (term.startsWith(QLatin1String("rating>="))) {
165         const QString value = term.mid(8);
166         const int stars = value.toInt() / 2;
167         setRating(stars);
168     } else if (term.startsWith(QLatin1String("tag:")) ||
169                term.startsWith(QLatin1String("tag="))) {
170         const QString value = term.mid(4);
171         addSearchTag(value);
172     }
173 }
174 
setFacetType(const QString & type)175 void DolphinFacetsWidget::setFacetType(const QString& type)
176 {
177     for (int index = 0; index <= m_typeSelector->count(); index++) {
178         if (type == m_typeSelector->itemData(index).toString()) {
179             m_typeSelector->setCurrentIndex(index);
180             break;
181         }
182     }
183 }
184 
setRating(const int stars)185 void DolphinFacetsWidget::setRating(const int stars)
186 {
187     if (stars < 0 || stars > 5) {
188         return;
189     }
190     m_ratingSelector->setCurrentIndex(stars);
191 }
192 
setTimespan(const QDate & date)193 void DolphinFacetsWidget::setTimespan(const QDate& date)
194 {
195     if (!date.isValid()) {
196         return;
197     }
198     m_dateSelector->setCurrentIndex(0);
199     for (int index = 1; index <= m_dateSelector->count(); index++) {
200         if (date >= m_dateSelector->itemData(index).toDate()) {
201             m_dateSelector->setCurrentIndex(index);
202             break;
203         }
204     }
205 }
206 
addSearchTag(const QString & tag)207 void DolphinFacetsWidget::addSearchTag(const QString& tag)
208 {
209     if (tag.isEmpty() || m_searchTags.contains(tag)) {
210         return;
211     }
212     m_searchTags.append(tag);
213     m_searchTags.sort();
214     updateTagsSelector();
215 }
216 
removeSearchTag(const QString & tag)217 void DolphinFacetsWidget::removeSearchTag(const QString& tag)
218 {
219     if (tag.isEmpty() || !m_searchTags.contains(tag)) {
220         return;
221     }
222     m_searchTags.removeAll(tag);
223     updateTagsSelector();
224 }
225 
resetSearchTags()226 void DolphinFacetsWidget::resetSearchTags()
227 {
228     m_searchTags = QStringList();
229     updateTagsSelector();
230     updateTagsMenu();
231 }
232 
initComboBox(QComboBox * combo)233 void DolphinFacetsWidget::initComboBox(QComboBox* combo)
234 {
235     combo->setFrame(false);
236     combo->setMinimumHeight(parentWidget()->height());
237     combo->setCurrentIndex(0);
238     connect(combo, QOverload<int>::of(&QComboBox::activated), this, &DolphinFacetsWidget::facetChanged);
239 }
240 
updateTagsSelector()241 void DolphinFacetsWidget::updateTagsSelector()
242 {
243     const bool hasListedTags = !m_tagsSelector->menu()->isEmpty();
244     const bool hasSelectedTags = !m_searchTags.isEmpty();
245 
246     if (hasSelectedTags) {
247         const QString tagsText = m_searchTags.join(i18nc("String list separator", ", "));
248         m_tagsSelector->setText(i18ncp("@action:button %2 is a list of tags",
249                                        "Tag: %2", "Tags: %2",m_searchTags.count(), tagsText));
250     } else {
251         m_tagsSelector->setText(i18nc("@action:button", "Add Tags"));
252     }
253 
254     m_tagsSelector->setEnabled(isEnabled() && (hasListedTags || hasSelectedTags));
255     m_clearTagsAction->setEnabled(hasSelectedTags);
256 }
257 
updateTagsMenu()258 void DolphinFacetsWidget::updateTagsMenu()
259 {
260     updateTagsMenuItems({}, {});
261     if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
262         m_tagsLister.openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload);
263     }
264 }
265 
updateTagsMenuItems(const QUrl &,const KFileItemList & items)266 void DolphinFacetsWidget::updateTagsMenuItems(const QUrl&, const KFileItemList& items)
267 {
268     QMenu *tagsMenu = m_tagsSelector->menu();
269     tagsMenu->clear();
270 
271     QStringList allTags = QStringList(m_searchTags);
272     for (const KFileItem &item: items) {
273         allTags.append(item.name());
274     }
275     allTags.sort(Qt::CaseInsensitive);
276     allTags.removeDuplicates();
277 
278     const bool onlyOneTag = allTags.count() == 1;
279 
280     for (const QString& tagName : qAsConst(allTags)) {
281         QAction *action = tagsMenu->addAction(QIcon::fromTheme(QStringLiteral("tag")), tagName);
282         action->setCheckable(true);
283         action->setChecked(m_searchTags.contains(tagName));
284 
285         connect(action, &QAction::triggered, this, [this, tagName, onlyOneTag](bool isChecked) {
286             if (isChecked) {
287                 addSearchTag(tagName);
288             } else {
289                 removeSearchTag(tagName);
290             }
291             Q_EMIT facetChanged();
292 
293             if (!onlyOneTag) {
294                 m_tagsSelector->menu()->show();
295             }
296         });
297     }
298 
299     if (allTags.count() > 1) {
300         tagsMenu->addSeparator();
301         tagsMenu->addAction(m_clearTagsAction);
302     }
303 
304     updateTagsSelector();
305 }
306