1 /*
2     This file is part of the KDE Baloo Project
3     SPDX-FileCopyrightText: 2014 Vishesh Handa <me@vhanda.in>
4     SPDX-FileCopyrightText: 2019 Tomaz Canabrava <tcanabrava@kde.org>
5     SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
6 
7     SPDX-License-Identifier: LGPL-2.1-or-later
8 
9 */
10 
11 #include "filteredfoldermodel.h"
12 
13 #include <QIcon>
14 
15 #include <KLocalizedString>
16 #include <QDir>
17 #include <QStringList>
18 #include <QTimer>
19 #include <QUrl>
20 
21 #include "baloo/baloosettings.h"
22 
23 namespace
24 {
normalizeTrailingSlashes(QString && input)25 QString normalizeTrailingSlashes(QString &&input)
26 {
27     if (!input.endsWith('/'))
28         return input + QLatin1Char('/');
29     return input;
30 }
31 
addTrailingSlashes(QStringList && list)32 QStringList addTrailingSlashes(QStringList &&list)
33 {
34     for (QString &str : list) {
35         str = normalizeTrailingSlashes(std::move(str));
36     }
37     return list;
38 }
39 
makeHomePretty(const QString & url)40 QString makeHomePretty(const QString &url)
41 {
42     if (url.startsWith(QDir::homePath()))
43         return QString(url).replace(0, QDir::homePath().length(), QStringLiteral("~"));
44     return url;
45 }
46 }
47 
FilteredFolderModel(BalooSettings * settings,QObject * parent)48 FilteredFolderModel::FilteredFolderModel(BalooSettings *settings, QObject *parent)
49     : QAbstractListModel(parent)
50     , m_settings(settings)
51 {
52 }
53 
updateDirectoryList()54 void FilteredFolderModel::updateDirectoryList()
55 {
56     beginResetModel();
57 
58     const QStringList runtimeExcluded = m_runtimeConfig.excludeFolders();
59 
60     QStringList settingsIncluded = addTrailingSlashes(m_settings->folders());
61     QStringList settingsExcluded = addTrailingSlashes(m_settings->excludedFolders());
62 
63     const QString homePath = normalizeTrailingSlashes(QDir::homePath());
64 
65     auto folderListEntry = [&homePath](const QString &url, bool include, bool fromConfig) {
66         QString displayName = url;
67         if (displayName.size() > 1) {
68             displayName.chop(1);
69         }
70         if (displayName.startsWith(homePath)) {
71             displayName.replace(0, homePath.length(), QStringLiteral("~/"));
72         }
73 
74         QString icon = QStringLiteral("folder");
75         if (url == homePath) {
76             icon = QStringLiteral("user-home");
77         } else if (!fromConfig) {
78             icon = QStringLiteral("drive-harddisk");
79         }
80 
81         return FolderInfo{url, displayName, icon, include, fromConfig};
82     };
83     m_folderList.clear();
84 
85     for (const QString &folder : settingsIncluded) {
86         m_folderList.append(folderListEntry(folder, true, true));
87     }
88     for (const QString &folder : settingsExcluded) {
89         m_folderList.append(folderListEntry(folder, false, true));
90     }
91 
92     // Add any automatically excluded mounts to the list
93     for (const QString &folder : runtimeExcluded) {
94         if (settingsIncluded.contains(folder) || settingsExcluded.contains(folder)) {
95             // Do not add any duplicates
96             continue;
97         }
98         if (m_deletedSettings.contains(folder)) {
99             // Skip entries deleted from the config
100             continue;
101         }
102         m_folderList.append(folderListEntry(folder, false, false));
103     }
104 
105     std::sort(m_folderList.begin(), m_folderList.end(), [](const FolderInfo &a, const FolderInfo &b) {
106         return a.url < b.url;
107     });
108 
109     endResetModel();
110 }
111 
data(const QModelIndex & idx,int role) const112 QVariant FilteredFolderModel::data(const QModelIndex &idx, int role) const
113 {
114     if (!idx.isValid() || idx.row() >= m_folderList.size()) {
115         return {};
116     }
117 
118     const auto entry = m_folderList.at(idx.row());
119     switch (role) {
120     case Qt::DisplayRole:
121         return entry.displayName;
122     case Qt::WhatsThisRole:
123         return entry.url;
124     case Qt::DecorationRole:
125         return entry.icon;
126     case Qt::ToolTipRole:
127         return makeHomePretty(entry.url);
128     case Url:
129         return entry.url;
130     case Folder:
131         return entry.displayName;
132     case EnableIndex:
133         return entry.enableIndex;
134     case Deletable:
135         return entry.isFromConfig && entry.url != normalizeTrailingSlashes(QDir::homePath());
136     default:
137         return {};
138     }
139 }
140 
setData(const QModelIndex & idx,const QVariant & value,int role)141 bool FilteredFolderModel::setData(const QModelIndex &idx, const QVariant &value, int role)
142 {
143     if (!idx.isValid() || idx.row() >= m_folderList.size()) {
144         return false;
145     }
146     FolderInfo &entry = m_folderList[idx.row()];
147     if (role == EnableIndex) {
148         entry.enableIndex = value.toBool();
149         syncFolderConfig(entry);
150         Q_EMIT dataChanged(idx, idx);
151         return true;
152     }
153     return false;
154 }
155 
rowCount(const QModelIndex & parent) const156 int FilteredFolderModel::rowCount(const QModelIndex &parent) const
157 {
158     Q_UNUSED(parent);
159     return m_folderList.count();
160 }
161 
addFolder(const QString & url,const bool included=false)162 void FilteredFolderModel::addFolder(const QString &url, const bool included = false)
163 {
164     QString nUrl = normalizeTrailingSlashes(QUrl(url).toLocalFile());
165 
166     auto it = std::find_if(m_folderList.begin(), m_folderList.end(), [nUrl](const FolderInfo &folder) {
167         return folder.url == nUrl;
168     });
169     if (it != m_folderList.end() && (*it).isFromConfig) {
170         return;
171     }
172     if (included) {
173         auto included = addTrailingSlashes(m_settings->folders());
174         included.append(nUrl);
175         std::sort(std::begin(included), std::end(included));
176         m_settings->setFolders(included);
177     } else {
178         auto excluded = addTrailingSlashes(m_settings->excludedFolders());
179         excluded.append(nUrl);
180         std::sort(std::begin(excluded), std::end(excluded));
181         m_settings->setExcludedFolders(excluded);
182     }
183     m_deletedSettings.removeAll(nUrl);
184 }
185 
removeFolder(int row)186 void FilteredFolderModel::removeFolder(int row)
187 {
188     auto entry = m_folderList.at(row);
189     if (!entry.isFromConfig) {
190         return;
191     }
192     if (!entry.enableIndex) {
193         auto excluded = addTrailingSlashes(m_settings->excludedFolders());
194         excluded.removeAll(entry.url);
195         std::sort(std::begin(excluded), std::end(excluded));
196         m_settings->setExcludedFolders(excluded);
197     } else {
198         auto included = addTrailingSlashes(m_settings->folders());
199         included.removeAll(entry.url);
200         std::sort(std::begin(included), std::end(included));
201         m_settings->setFolders(included);
202     }
203     m_deletedSettings.append(entry.url);
204 }
205 
syncFolderConfig(const FolderInfo & entry)206 void FilteredFolderModel::syncFolderConfig(const FolderInfo &entry)
207 {
208     auto excluded = addTrailingSlashes(m_settings->excludedFolders());
209     auto included = addTrailingSlashes(m_settings->folders());
210     if (entry.enableIndex) {
211         included.append(entry.url);
212         std::sort(std::begin(included), std::end(included));
213         if (excluded.removeAll(entry.url)) {
214             std::sort(std::begin(excluded), std::end(excluded));
215             m_settings->setExcludedFolders(excluded);
216         }
217         m_settings->setFolders(included);
218     } else {
219         excluded.append(entry.url);
220         std::sort(std::begin(excluded), std::end(excluded));
221         if (included.removeAll(entry.url)) {
222             std::sort(std::begin(included), std::end(included));
223             m_settings->setFolders(included);
224         }
225         m_settings->setExcludedFolders(excluded);
226     }
227 }
228 
roleNames() const229 QHash<int, QByteArray> FilteredFolderModel::roleNames() const
230 {
231     return {{Url, "url"}, //
232             {Folder, "folder"},
233             {EnableIndex, "enableIndex"},
234             {Deletable, "deletable"},
235             {Qt::DecorationRole, "decoration"}};
236 }
237