]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/viewproperties.cpp
Implemented the possibility for sorting/grouping behaviors that are not tied to roles...
[dolphin.git] / src / views / viewproperties.cpp
1 /*
2 * SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "viewproperties.h"
9
10 #include "dolphin_directoryviewpropertysettings.h"
11 #include "dolphin_generalsettings.h"
12 #include "dolphindebug.h"
13
14 #include <QCryptographicHash>
15
16 #include <KFileItem>
17
18 namespace
19 {
20 const int AdditionalInfoViewPropertiesVersion = 1;
21 const int NameRolePropertiesVersion = 2;
22 const int DateRolePropertiesVersion = 4;
23 const int CurrentViewPropertiesVersion = 4;
24
25 // String representation to mark the additional properties of
26 // the details view as customized by the user. See
27 // ViewProperties::visibleRoles() for more information.
28 const char CustomizedDetailsString[] = "CustomizedDetails";
29
30 // Filename that is used for storing the properties
31 const char ViewPropertiesFileName[] = ".directory";
32 }
33
34 ViewProperties::ViewProperties(const QUrl &url)
35 : m_changedProps(false)
36 , m_autoSave(true)
37 , m_node(nullptr)
38 {
39 GeneralSettings *settings = GeneralSettings::self();
40 const bool useGlobalViewProps = settings->globalViewProps() || url.isEmpty();
41 bool useSearchView = false;
42 bool useTrashView = false;
43 bool useRecentDocumentsView = false;
44 bool useDownloadsView = false;
45
46 // We try and save it to the file .directory in the directory being viewed.
47 // If the directory is not writable by the user or the directory is not local,
48 // we store the properties information in a local file.
49 if (url.scheme().contains(QLatin1String("search"))) {
50 m_filePath = destinationDir(QStringLiteral("search/")) + directoryHashForUrl(url);
51 useSearchView = true;
52 } else if (url.scheme() == QLatin1String("trash")) {
53 m_filePath = destinationDir(QStringLiteral("trash"));
54 useTrashView = true;
55 } else if (url.scheme() == QLatin1String("recentlyused")) {
56 m_filePath = destinationDir(QStringLiteral("recentlyused"));
57 useRecentDocumentsView = true;
58 } else if (url.scheme() == QLatin1String("timeline")) {
59 m_filePath = destinationDir(QStringLiteral("timeline"));
60 useRecentDocumentsView = true;
61 } else if (useGlobalViewProps) {
62 m_filePath = destinationDir(QStringLiteral("global"));
63 } else if (url.isLocalFile()) {
64 m_filePath = url.toLocalFile();
65
66 bool useDestinationDir = !isPartOfHome(m_filePath);
67 if (!useDestinationDir) {
68 const KFileItem fileItem(url);
69 useDestinationDir = fileItem.isSlow();
70 }
71
72 if (!useDestinationDir) {
73 const QFileInfo dirInfo(m_filePath);
74 const QFileInfo fileInfo(m_filePath + QDir::separator() + ViewPropertiesFileName);
75 useDestinationDir = !dirInfo.isWritable() || (dirInfo.size() > 0 && fileInfo.exists() && !(fileInfo.isReadable() && fileInfo.isWritable()));
76 }
77
78 if (useDestinationDir) {
79 #ifdef Q_OS_WIN
80 // m_filePath probably begins with C:/ - the colon is not a valid character for paths though
81 m_filePath = QDir::separator() + m_filePath.remove(QLatin1Char(':'));
82 #endif
83 m_filePath = destinationDir(QStringLiteral("local")) + m_filePath;
84 }
85
86 if (m_filePath == QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)) {
87 useDownloadsView = true;
88 }
89 } else {
90 m_filePath = destinationDir(QStringLiteral("remote")) + m_filePath;
91 }
92
93 const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
94 m_node = new ViewPropertySettings(KSharedConfig::openConfig(file));
95
96 // If the .directory file does not exist or the timestamp is too old,
97 // use default values instead.
98 const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
99 && (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp()));
100 if (useDefaultProps) {
101 if (useSearchView) {
102 const QString path = url.path();
103
104 if (path == QLatin1String("/images")) {
105 setViewMode(DolphinView::IconsView);
106 setPreviewsShown(true);
107 setVisibleRoles({"text", "dimensions", "imageDateTime"});
108 } else if (path == QLatin1String("/audio")) {
109 setViewMode(DolphinView::DetailsView);
110 setVisibleRoles({"text", "artist", "album", "duration"});
111 } else if (path == QLatin1String("/videos")) {
112 setViewMode(DolphinView::IconsView);
113 setPreviewsShown(true);
114 setVisibleRoles({"text"});
115 } else {
116 setViewMode(DolphinView::DetailsView);
117 setVisibleRoles({"text", "path", "modificationtime"});
118 }
119 } else if (useTrashView) {
120 setViewMode(DolphinView::DetailsView);
121 setVisibleRoles({"text", "path", "deletiontime"});
122 } else if (useRecentDocumentsView || useDownloadsView) {
123 setSortRole(QByteArrayLiteral("modificationtime"));
124 setSortOrder(Qt::DescendingOrder);
125 setSortFoldersFirst(false);
126 setGroupedSorting(true);
127
128 if (useRecentDocumentsView) {
129 setViewMode(DolphinView::DetailsView);
130 setVisibleRoles({"text", "path", "modificationtime"});
131 }
132 } else {
133 // The global view-properties act as default for directories without
134 // any view-property configuration. Constructing a ViewProperties
135 // instance for an empty QUrl ensures that the global view-properties
136 // are loaded.
137 QUrl emptyUrl;
138 ViewProperties defaultProps(emptyUrl);
139 setDirProperties(defaultProps);
140
141 m_changedProps = false;
142 }
143 }
144
145 if (m_node->version() < CurrentViewPropertiesVersion) {
146 // The view-properties have an outdated version. Convert the properties
147 // to the changes of the current version.
148 if (m_node->version() < AdditionalInfoViewPropertiesVersion) {
149 convertAdditionalInfo();
150 Q_ASSERT(m_node->version() == AdditionalInfoViewPropertiesVersion);
151 }
152
153 if (m_node->version() < NameRolePropertiesVersion) {
154 convertNameRoleToTextRole();
155 Q_ASSERT(m_node->version() == NameRolePropertiesVersion);
156 }
157
158 if (m_node->version() < DateRolePropertiesVersion) {
159 convertDateRoleToModificationTimeRole();
160 Q_ASSERT(m_node->version() == DateRolePropertiesVersion);
161 }
162
163 m_node->setVersion(CurrentViewPropertiesVersion);
164 }
165 }
166
167 ViewProperties::~ViewProperties()
168 {
169 if (m_changedProps && m_autoSave) {
170 save();
171 }
172
173 delete m_node;
174 m_node = nullptr;
175 }
176
177 void ViewProperties::setViewMode(DolphinView::Mode mode)
178 {
179 if (m_node->viewMode() != mode) {
180 m_node->setViewMode(mode);
181 update();
182 }
183 }
184
185 DolphinView::Mode ViewProperties::viewMode() const
186 {
187 const int mode = qBound(0, m_node->viewMode(), 2);
188 return static_cast<DolphinView::Mode>(mode);
189 }
190
191 void ViewProperties::setPreviewsShown(bool show)
192 {
193 if (m_node->previewsShown() != show) {
194 m_node->setPreviewsShown(show);
195 update();
196 }
197 }
198
199 bool ViewProperties::previewsShown() const
200 {
201 return m_node->previewsShown();
202 }
203
204 void ViewProperties::setHiddenFilesShown(bool show)
205 {
206 if (m_node->hiddenFilesShown() != show) {
207 m_node->setHiddenFilesShown(show);
208 update();
209 }
210 }
211
212 void ViewProperties::setGroupedSorting(bool grouped)
213 {
214 if (m_node->groupedSorting() != grouped) {
215 m_node->setGroupedSorting(grouped);
216 update();
217 }
218 }
219
220 bool ViewProperties::groupedSorting() const
221 {
222 return m_node->groupedSorting();
223 }
224
225 bool ViewProperties::hiddenFilesShown() const
226 {
227 return m_node->hiddenFilesShown();
228 }
229
230 void ViewProperties::setSortRole(const QByteArray &role)
231 {
232 if (m_node->sortRole() != role) {
233 m_node->setSortRole(role);
234 update();
235 }
236 }
237
238 QByteArray ViewProperties::sortRole() const
239 {
240 return m_node->sortRole().toLatin1();
241 }
242
243 void ViewProperties::setSortOrder(Qt::SortOrder sortOrder)
244 {
245 if (m_node->sortOrder() != sortOrder) {
246 m_node->setSortOrder(sortOrder);
247 update();
248 }
249 }
250
251 Qt::SortOrder ViewProperties::sortOrder() const
252 {
253 return static_cast<Qt::SortOrder>(m_node->sortOrder());
254 }
255
256 void ViewProperties::setGroupRole(const QByteArray &role)
257 {
258 if (m_node->groupRole() != role) {
259 m_node->setGroupRole(role);
260 update();
261 }
262 }
263
264 QByteArray ViewProperties::groupRole() const
265 {
266 return m_node->groupRole().toLatin1();
267 }
268
269 void ViewProperties::setGroupOrder(Qt::SortOrder groupOrder)
270 {
271 if (m_node->groupOrder() != groupOrder) {
272 m_node->setGroupOrder(groupOrder);
273 update();
274 }
275 }
276
277 Qt::SortOrder ViewProperties::groupOrder() const
278 {
279 return static_cast<Qt::SortOrder>(m_node->groupOrder());
280 }
281
282 void ViewProperties::setSortFoldersFirst(bool foldersFirst)
283 {
284 if (m_node->sortFoldersFirst() != foldersFirst) {
285 m_node->setSortFoldersFirst(foldersFirst);
286 update();
287 }
288 }
289
290 bool ViewProperties::sortFoldersFirst() const
291 {
292 return m_node->sortFoldersFirst();
293 }
294
295 void ViewProperties::setSortHiddenLast(bool hiddenLast)
296 {
297 if (m_node->sortHiddenLast() != hiddenLast) {
298 m_node->setSortHiddenLast(hiddenLast);
299 update();
300 }
301 }
302
303 bool ViewProperties::sortHiddenLast() const
304 {
305 return m_node->sortHiddenLast();
306 }
307
308 void ViewProperties::setVisibleRoles(const QList<QByteArray> &roles)
309 {
310 if (roles == visibleRoles()) {
311 return;
312 }
313
314 // See ViewProperties::visibleRoles() for the storage format
315 // of the additional information.
316
317 // Remove the old values stored for the current view-mode
318 const QStringList oldVisibleRoles = m_node->visibleRoles();
319 const QString prefix = viewModePrefix();
320 QStringList newVisibleRoles = oldVisibleRoles;
321 for (int i = newVisibleRoles.count() - 1; i >= 0; --i) {
322 if (newVisibleRoles[i].startsWith(prefix)) {
323 newVisibleRoles.removeAt(i);
324 }
325 }
326
327 // Add the updated values for the current view-mode
328 newVisibleRoles.reserve(roles.count());
329 for (const QByteArray &role : roles) {
330 newVisibleRoles.append(prefix + role);
331 }
332
333 if (oldVisibleRoles != newVisibleRoles) {
334 const bool markCustomizedDetails = (m_node->viewMode() == DolphinView::DetailsView) && !newVisibleRoles.contains(CustomizedDetailsString);
335 if (markCustomizedDetails) {
336 // The additional information of the details-view has been modified. Set a marker,
337 // so that it is allowed to also show no additional information without doing the
338 // fallback to show the size and date per default.
339 newVisibleRoles.append(CustomizedDetailsString);
340 }
341
342 m_node->setVisibleRoles(newVisibleRoles);
343 update();
344 }
345 }
346
347 QList<QByteArray> ViewProperties::visibleRoles() const
348 {
349 // The shown additional information is stored for each view-mode separately as
350 // string with the view-mode as prefix. Example:
351 //
352 // AdditionalInfo=Details_size,Details_date,Details_owner,Icons_size
353 //
354 // To get the representation as QList<QByteArray>, the current
355 // view-mode must be checked and the values of this mode added to the list.
356 //
357 // For the details-view a special case must be respected: Per default the size
358 // and date should be shown without creating a .directory file. Only if
359 // the user explicitly has modified the properties of the details view (marked
360 // by "CustomizedDetails"), also a details-view with no additional information
361 // is accepted.
362
363 QList<QByteArray> roles{"text"};
364
365 // Iterate through all stored keys and append all roles that match to
366 // the current view mode.
367 const QString prefix = viewModePrefix();
368 const int prefixLength = prefix.length();
369
370 const QStringList visibleRoles = m_node->visibleRoles();
371 for (const QString &visibleRole : visibleRoles) {
372 if (visibleRole.startsWith(prefix)) {
373 const QByteArray role = visibleRole.right(visibleRole.length() - prefixLength).toLatin1();
374 if (role != "text") {
375 roles.append(role);
376 }
377 }
378 }
379
380 // For the details view the size and date should be shown per default
381 // until the additional information has been explicitly changed by the user
382 const bool useDefaultValues = roles.count() == 1 // "text"
383 && (m_node->viewMode() == DolphinView::DetailsView) && !visibleRoles.contains(CustomizedDetailsString);
384 if (useDefaultValues) {
385 roles.append("size");
386 roles.append("modificationtime");
387 }
388
389 return roles;
390 }
391
392 void ViewProperties::setHeaderColumnWidths(const QList<int> &widths)
393 {
394 if (m_node->headerColumnWidths() != widths) {
395 m_node->setHeaderColumnWidths(widths);
396 update();
397 }
398 }
399
400 QList<int> ViewProperties::headerColumnWidths() const
401 {
402 return m_node->headerColumnWidths();
403 }
404
405 void ViewProperties::setDirProperties(const ViewProperties &props)
406 {
407 setViewMode(props.viewMode());
408 setPreviewsShown(props.previewsShown());
409 setHiddenFilesShown(props.hiddenFilesShown());
410 setGroupedSorting(props.groupedSorting());
411 setSortRole(props.sortRole());
412 setSortOrder(props.sortOrder());
413 setGroupRole(props.groupRole());
414 setGroupOrder(props.groupOrder());
415 setSortFoldersFirst(props.sortFoldersFirst());
416 setSortHiddenLast(props.sortHiddenLast());
417 setVisibleRoles(props.visibleRoles());
418 setHeaderColumnWidths(props.headerColumnWidths());
419 m_node->setVersion(props.m_node->version());
420 }
421
422 void ViewProperties::setAutoSaveEnabled(bool autoSave)
423 {
424 m_autoSave = autoSave;
425 }
426
427 bool ViewProperties::isAutoSaveEnabled() const
428 {
429 return m_autoSave;
430 }
431
432 void ViewProperties::update()
433 {
434 m_changedProps = true;
435 m_node->setTimestamp(QDateTime::currentDateTime());
436 }
437
438 void ViewProperties::save()
439 {
440 qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath;
441 QDir dir;
442 dir.mkpath(m_filePath);
443 m_node->setVersion(CurrentViewPropertiesVersion);
444 m_node->save();
445 m_changedProps = false;
446 }
447
448 bool ViewProperties::exist() const
449 {
450 const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
451 return QFile::exists(file);
452 }
453
454 QString ViewProperties::destinationDir(const QString &subDir) const
455 {
456 QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
457 path.append("/view_properties/").append(subDir);
458 return path;
459 }
460
461 QString ViewProperties::viewModePrefix() const
462 {
463 QString prefix;
464
465 switch (m_node->viewMode()) {
466 case DolphinView::IconsView:
467 prefix = QStringLiteral("Icons_");
468 break;
469 case DolphinView::CompactView:
470 prefix = QStringLiteral("Compact_");
471 break;
472 case DolphinView::DetailsView:
473 prefix = QStringLiteral("Details_");
474 break;
475 default:
476 qCWarning(DolphinDebug) << "Unknown view-mode of the view properties";
477 }
478
479 return prefix;
480 }
481
482 void ViewProperties::convertAdditionalInfo()
483 {
484 QStringList visibleRoles = m_node->visibleRoles();
485
486 const QStringList additionalInfo = m_node->additionalInfo();
487 if (!additionalInfo.isEmpty()) {
488 // Convert the obsolete values like Icons_Size, Details_Date, ...
489 // to Icons_size, Details_date, ... where the suffix just represents
490 // the internal role. One special-case must be handled: "LinkDestination"
491 // has been used for "destination".
492 visibleRoles.reserve(visibleRoles.count() + additionalInfo.count());
493 for (const QString &info : additionalInfo) {
494 QString visibleRole = info;
495 int index = visibleRole.indexOf('_');
496 if (index >= 0 && index + 1 < visibleRole.length()) {
497 ++index;
498 if (visibleRole[index] == QLatin1Char('L')) {
499 visibleRole.replace(QLatin1String("LinkDestination"), QLatin1String("destination"));
500 } else {
501 visibleRole[index] = visibleRole[index].toLower();
502 }
503 }
504 if (!visibleRoles.contains(visibleRole)) {
505 visibleRoles.append(visibleRole);
506 }
507 }
508 }
509
510 m_node->setAdditionalInfo(QStringList());
511 m_node->setVisibleRoles(visibleRoles);
512 m_node->setVersion(AdditionalInfoViewPropertiesVersion);
513 update();
514 }
515
516 void ViewProperties::convertNameRoleToTextRole()
517 {
518 QStringList visibleRoles = m_node->visibleRoles();
519 for (int i = 0; i < visibleRoles.count(); ++i) {
520 if (visibleRoles[i].endsWith(QLatin1String("_name"))) {
521 const int leftLength = visibleRoles[i].length() - 5;
522 visibleRoles[i] = visibleRoles[i].left(leftLength) + "_text";
523 }
524 }
525
526 QString sortRole = m_node->sortRole();
527 if (sortRole == QLatin1String("name")) {
528 sortRole = QStringLiteral("text");
529 }
530
531 m_node->setVisibleRoles(visibleRoles);
532 m_node->setSortRole(sortRole);
533 m_node->setVersion(NameRolePropertiesVersion);
534 update();
535 }
536
537 void ViewProperties::convertDateRoleToModificationTimeRole()
538 {
539 QStringList visibleRoles = m_node->visibleRoles();
540 for (int i = 0; i < visibleRoles.count(); ++i) {
541 if (visibleRoles[i].endsWith(QLatin1String("_date"))) {
542 const int leftLength = visibleRoles[i].length() - 5;
543 visibleRoles[i] = visibleRoles[i].left(leftLength) + "_modificationtime";
544 }
545 }
546
547 QString sortRole = m_node->sortRole();
548 if (sortRole == QLatin1String("date")) {
549 sortRole = QStringLiteral("modificationtime");
550 }
551
552 m_node->setVisibleRoles(visibleRoles);
553 m_node->setSortRole(sortRole);
554 m_node->setVersion(DateRolePropertiesVersion);
555 update();
556 }
557
558 bool ViewProperties::isPartOfHome(const QString &filePath)
559 {
560 // For performance reasons cache the path in a static QString
561 // (see QDir::homePath() for more details)
562 static QString homePath;
563 if (homePath.isEmpty()) {
564 homePath = QDir::homePath();
565 Q_ASSERT(!homePath.isEmpty());
566 }
567
568 return filePath.startsWith(homePath);
569 }
570
571 QString ViewProperties::directoryHashForUrl(const QUrl &url)
572 {
573 const QByteArray hashValue = QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Sha1);
574 QString hashString = hashValue.toBase64();
575 hashString.replace('/', '-');
576 return hashString;
577 }