2 * SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
5 * SPDX-License-Identifier: GPL-2.0-or-later
8 #include "viewproperties.h"
10 #include "dolphin_directoryviewpropertysettings.h"
11 #include "dolphin_generalsettings.h"
12 #include "dolphindebug.h"
14 #include <QCryptographicHash>
15 #include <QTemporaryFile>
18 #include <KFileMetaData/UserMetaData>
22 const int AdditionalInfoViewPropertiesVersion
= 1;
23 const int NameRolePropertiesVersion
= 2;
24 const int DateRolePropertiesVersion
= 4;
25 const int CurrentViewPropertiesVersion
= 4;
27 // String representation to mark the additional properties of
28 // the details view as customized by the user. See
29 // ViewProperties::visibleRoles() for more information.
30 const char CustomizedDetailsString
[] = "CustomizedDetails";
32 // Filename that is used for storing the properties
33 const char ViewPropertiesFileName
[] = ".directory";
36 ViewPropertySettings
*ViewProperties::loadProperties(const QString
&folderPath
) const
38 const QString settingsFile
= folderPath
+ QDir::separator() + ViewPropertiesFileName
;
40 KFileMetaData::UserMetaData
metadata(folderPath
);
41 if (!metadata
.isSupported()) {
42 return new ViewPropertySettings(KSharedConfig::openConfig(settingsFile
, KConfig::SimpleConfig
));
45 std::unique_ptr
<QTemporaryFile
> tempFile(new QTemporaryFile());
46 tempFile
->setAutoRemove(false);
47 if (!tempFile
->open()) {
48 qCWarning(DolphinDebug
) << "Could not open temp file";
51 if (QFile::exists(settingsFile
)) {
52 // copy settings to tempfile to load them separately
53 QFile::remove(tempFile
->fileName());
54 QFile::copy(settingsFile
, tempFile
->fileName());
56 auto config
= KConfig(tempFile
->fileName(), KConfig::SimpleConfig
);
57 // ignore settings that are outside of dolphin scope
58 if (config
.hasGroup("Dolphin") || config
.hasGroup("Settings")) {
59 const auto groupList
= config
.groupList();
60 for (const auto &group
: groupList
) {
61 if (group
!= QStringLiteral("Dolphin") && group
!= QStringLiteral("Settings")) {
62 config
.deleteGroup(group
);
65 return new ViewPropertySettings(KSharedConfig::openConfig(tempFile
->fileName(), KConfig::SimpleConfig
));
67 } else if (!config
.groupList().isEmpty()) {
68 // clear temp file content
69 QFile::remove(tempFile
->fileName());
74 const QString viewPropertiesString
= metadata
.attribute(QStringLiteral("kde.fm.viewproperties#1"));
75 if (viewPropertiesString
.isEmpty()) {
78 // load view properties from xattr to temp file then loads into ViewPropertySettings
79 QFile
outputFile(tempFile
->fileName());
80 outputFile
.open(QIODevice::WriteOnly
);
81 outputFile
.write(viewPropertiesString
.toUtf8());
83 return new ViewPropertySettings(KSharedConfig::openConfig(tempFile
->fileName(), KConfig::SimpleConfig
));
86 ViewPropertySettings
*ViewProperties::defaultProperties() const
88 auto props
= loadProperties(destinationDir(QStringLiteral("global")));
89 if (props
== nullptr) {
90 qCWarning(DolphinDebug
) << "Could not load default global viewproperties";
91 QTemporaryFile tempFile
;
92 tempFile
.setAutoRemove(false);
93 if (!tempFile
.open()) {
94 qCWarning(DolphinDebug
) << "Could not open temp file";
95 props
= new ViewPropertySettings
;
97 props
= new ViewPropertySettings(KSharedConfig::openConfig(tempFile
.fileName(), KConfig::SimpleConfig
));
104 ViewProperties::ViewProperties(const QUrl
&url
)
105 : m_changedProps(false)
109 GeneralSettings
*settings
= GeneralSettings::self();
110 const bool useGlobalViewProps
= settings
->globalViewProps() || url
.isEmpty();
111 bool useSearchView
= false;
112 bool useTrashView
= false;
113 bool useRecentDocumentsView
= false;
114 bool useDownloadsView
= false;
116 // We try and save it to the file .directory in the directory being viewed.
117 // If the directory is not writable by the user or the directory is not local,
118 // we store the properties information in a local file.
119 if (url
.scheme().contains(QLatin1String("search"))) {
120 m_filePath
= destinationDir(QStringLiteral("search/")) + directoryHashForUrl(url
);
121 useSearchView
= true;
122 } else if (url
.scheme() == QLatin1String("trash")) {
123 m_filePath
= destinationDir(QStringLiteral("trash"));
125 } else if (url
.scheme() == QLatin1String("recentlyused")) {
126 m_filePath
= destinationDir(QStringLiteral("recentlyused"));
127 useRecentDocumentsView
= true;
128 } else if (url
.scheme() == QLatin1String("timeline")) {
129 m_filePath
= destinationDir(QStringLiteral("timeline"));
130 useRecentDocumentsView
= true;
131 } else if (useGlobalViewProps
) {
132 m_filePath
= destinationDir(QStringLiteral("global"));
133 } else if (url
.isLocalFile()) {
134 m_filePath
= url
.toLocalFile();
136 bool useDestinationDir
= !isPartOfHome(m_filePath
);
137 if (!useDestinationDir
) {
138 const KFileItem
fileItem(url
);
139 useDestinationDir
= fileItem
.isSlow();
142 if (!useDestinationDir
) {
143 const QFileInfo
dirInfo(m_filePath
);
144 const QFileInfo
fileInfo(m_filePath
+ QDir::separator() + ViewPropertiesFileName
);
145 useDestinationDir
= !dirInfo
.isWritable() || (dirInfo
.size() > 0 && fileInfo
.exists() && !(fileInfo
.isReadable() && fileInfo
.isWritable()));
148 if (useDestinationDir
) {
150 // m_filePath probably begins with C:/ - the colon is not a valid character for paths though
151 m_filePath
= QDir::separator() + m_filePath
.remove(QLatin1Char(':'));
153 m_filePath
= destinationDir(QStringLiteral("local")) + m_filePath
;
156 if (m_filePath
== QStandardPaths::writableLocation(QStandardPaths::DownloadLocation
)) {
157 useDownloadsView
= true;
160 m_filePath
= destinationDir(QStringLiteral("remote")) + m_filePath
;
163 m_node
= loadProperties(m_filePath
);
165 bool useDefaultSettings
= useGlobalViewProps
||
166 // If the props timestamp is too old,
167 // use default values instead.
168 (m_node
!= nullptr && (!useGlobalViewProps
|| useSearchView
|| useTrashView
|| useRecentDocumentsView
|| useDownloadsView
)
169 && m_node
->timestamp() < settings
->viewPropsTimestamp());
171 if (m_node
== nullptr) {
172 // no settings found for m_filepath, load defaults
173 m_node
= defaultProperties();
174 useDefaultSettings
= true;
177 // default values for special directories
178 if (useDefaultSettings
) {
180 const QString path
= url
.path();
182 if (path
== QLatin1String("/images")) {
183 setViewMode(DolphinView::IconsView
);
184 setPreviewsShown(true);
185 setVisibleRoles({"text", "dimensions", "imageDateTime"});
186 } else if (path
== QLatin1String("/audio")) {
187 setViewMode(DolphinView::DetailsView
);
188 setVisibleRoles({"text", "artist", "album", "duration"});
189 } else if (path
== QLatin1String("/videos")) {
190 setViewMode(DolphinView::IconsView
);
191 setPreviewsShown(true);
192 setVisibleRoles({"text"});
194 setViewMode(DolphinView::DetailsView
);
195 setVisibleRoles({"text", "path", "modificationtime"});
197 } else if (useTrashView
) {
198 setViewMode(DolphinView::DetailsView
);
199 setVisibleRoles({"text", "path", "deletiontime"});
200 } else if (useRecentDocumentsView
|| useDownloadsView
) {
201 setSortOrder(Qt::DescendingOrder
);
202 setSortFoldersFirst(false);
203 setGroupedSorting(true);
205 if (useRecentDocumentsView
) {
206 setSortRole(QByteArrayLiteral("accesstime"));
207 setViewMode(DolphinView::DetailsView
);
208 setVisibleRoles({"text", "path", "accesstime"});
210 setSortRole(QByteArrayLiteral("modificationtime"));
213 m_changedProps
= false;
217 if (m_node
->version() < CurrentViewPropertiesVersion
) {
218 // The view-properties have an outdated version. Convert the properties
219 // to the changes of the current version.
220 if (m_node
->version() < AdditionalInfoViewPropertiesVersion
) {
221 convertAdditionalInfo();
222 Q_ASSERT(m_node
->version() == AdditionalInfoViewPropertiesVersion
);
225 if (m_node
->version() < NameRolePropertiesVersion
) {
226 convertNameRoleToTextRole();
227 Q_ASSERT(m_node
->version() == NameRolePropertiesVersion
);
230 if (m_node
->version() < DateRolePropertiesVersion
) {
231 convertDateRoleToModificationTimeRole();
232 Q_ASSERT(m_node
->version() == DateRolePropertiesVersion
);
235 m_node
->setVersion(CurrentViewPropertiesVersion
);
239 ViewProperties::~ViewProperties()
241 if (m_changedProps
&& m_autoSave
) {
245 if (!m_node
->config()->name().endsWith(ViewPropertiesFileName
)) {
247 QFile::remove(m_node
->config()->name());
254 void ViewProperties::setViewMode(DolphinView::Mode mode
)
256 if (m_node
->viewMode() != mode
) {
257 m_node
->setViewMode(mode
);
262 DolphinView::Mode
ViewProperties::viewMode() const
264 const int mode
= qBound(0, m_node
->viewMode(), 2);
265 return static_cast<DolphinView::Mode
>(mode
);
268 void ViewProperties::setPreviewsShown(bool show
)
270 if (m_node
->previewsShown() != show
) {
271 m_node
->setPreviewsShown(show
);
276 bool ViewProperties::previewsShown() const
278 return m_node
->previewsShown();
281 void ViewProperties::setHiddenFilesShown(bool show
)
283 if (m_node
->hiddenFilesShown() != show
) {
284 m_node
->setHiddenFilesShown(show
);
289 void ViewProperties::setGroupedSorting(bool grouped
)
291 if (m_node
->groupedSorting() != grouped
) {
292 m_node
->setGroupedSorting(grouped
);
297 bool ViewProperties::groupedSorting() const
299 return m_node
->groupedSorting();
302 bool ViewProperties::hiddenFilesShown() const
304 return m_node
->hiddenFilesShown();
307 void ViewProperties::setSortRole(const QByteArray
&role
)
309 if (m_node
->sortRole() != role
) {
310 m_node
->setSortRole(role
);
315 QByteArray
ViewProperties::sortRole() const
317 return m_node
->sortRole().toLatin1();
320 void ViewProperties::setSortOrder(Qt::SortOrder sortOrder
)
322 if (m_node
->sortOrder() != sortOrder
) {
323 m_node
->setSortOrder(sortOrder
);
328 Qt::SortOrder
ViewProperties::sortOrder() const
330 return static_cast<Qt::SortOrder
>(m_node
->sortOrder());
333 void ViewProperties::setSortFoldersFirst(bool foldersFirst
)
335 if (m_node
->sortFoldersFirst() != foldersFirst
) {
336 m_node
->setSortFoldersFirst(foldersFirst
);
341 bool ViewProperties::sortFoldersFirst() const
343 return m_node
->sortFoldersFirst();
346 void ViewProperties::setSortHiddenLast(bool hiddenLast
)
348 if (m_node
->sortHiddenLast() != hiddenLast
) {
349 m_node
->setSortHiddenLast(hiddenLast
);
354 bool ViewProperties::sortHiddenLast() const
356 return m_node
->sortHiddenLast();
359 void ViewProperties::setDynamicViewPassed(bool dynamicViewPassed
)
361 if (m_node
->dynamicViewPassed() != dynamicViewPassed
) {
362 m_node
->setDynamicViewPassed(dynamicViewPassed
);
367 bool ViewProperties::dynamicViewPassed() const
369 return m_node
->dynamicViewPassed();
372 void ViewProperties::setVisibleRoles(const QList
<QByteArray
> &roles
)
374 if (roles
== visibleRoles()) {
378 // See ViewProperties::visibleRoles() for the storage format
379 // of the additional information.
381 // Remove the old values stored for the current view-mode
382 const QStringList oldVisibleRoles
= m_node
->visibleRoles();
383 const QString prefix
= viewModePrefix();
384 QStringList newVisibleRoles
= oldVisibleRoles
;
385 for (int i
= newVisibleRoles
.count() - 1; i
>= 0; --i
) {
386 if (newVisibleRoles
[i
].startsWith(prefix
)) {
387 newVisibleRoles
.removeAt(i
);
391 // Add the updated values for the current view-mode
392 newVisibleRoles
.reserve(roles
.count());
393 for (const QByteArray
&role
: roles
) {
394 newVisibleRoles
.append(prefix
+ role
);
397 if (oldVisibleRoles
!= newVisibleRoles
) {
398 const bool markCustomizedDetails
= (m_node
->viewMode() == DolphinView::DetailsView
) && !newVisibleRoles
.contains(CustomizedDetailsString
);
399 if (markCustomizedDetails
) {
400 // The additional information of the details-view has been modified. Set a marker,
401 // so that it is allowed to also show no additional information without doing the
402 // fallback to show the size and date per default.
403 newVisibleRoles
.append(CustomizedDetailsString
);
406 m_node
->setVisibleRoles(newVisibleRoles
);
411 QList
<QByteArray
> ViewProperties::visibleRoles() const
413 // The shown additional information is stored for each view-mode separately as
414 // string with the view-mode as prefix. Example:
416 // AdditionalInfo=Details_size,Details_date,Details_owner,Icons_size
418 // To get the representation as QList<QByteArray>, the current
419 // view-mode must be checked and the values of this mode added to the list.
421 // For the details-view a special case must be respected: Per default the size
422 // and date should be shown without creating a .directory file. Only if
423 // the user explicitly has modified the properties of the details view (marked
424 // by "CustomizedDetails"), also a details-view with no additional information
427 QList
<QByteArray
> roles
{"text"};
429 // Iterate through all stored keys and append all roles that match to
430 // the current view mode.
431 const QString prefix
= viewModePrefix();
432 const int prefixLength
= prefix
.length();
434 const QStringList visibleRoles
= m_node
->visibleRoles();
435 for (const QString
&visibleRole
: visibleRoles
) {
436 if (visibleRole
.startsWith(prefix
)) {
437 const QByteArray role
= visibleRole
.right(visibleRole
.length() - prefixLength
).toLatin1();
438 if (role
!= "text") {
444 // For the details view the size and date should be shown per default
445 // until the additional information has been explicitly changed by the user
446 const bool useDefaultValues
= roles
.count() == 1 // "text"
447 && (m_node
->viewMode() == DolphinView::DetailsView
) && !visibleRoles
.contains(CustomizedDetailsString
);
448 if (useDefaultValues
) {
449 roles
.append("size");
450 roles
.append("modificationtime");
456 void ViewProperties::setHeaderColumnWidths(const QList
<int> &widths
)
458 if (m_node
->headerColumnWidths() != widths
) {
459 m_node
->setHeaderColumnWidths(widths
);
464 QList
<int> ViewProperties::headerColumnWidths() const
466 return m_node
->headerColumnWidths();
469 void ViewProperties::setDirProperties(const ViewProperties
&props
)
471 setViewMode(props
.viewMode());
472 setPreviewsShown(props
.previewsShown());
473 setHiddenFilesShown(props
.hiddenFilesShown());
474 setGroupedSorting(props
.groupedSorting());
475 setSortRole(props
.sortRole());
476 setSortOrder(props
.sortOrder());
477 setSortFoldersFirst(props
.sortFoldersFirst());
478 setSortHiddenLast(props
.sortHiddenLast());
479 setVisibleRoles(props
.visibleRoles());
480 setHeaderColumnWidths(props
.headerColumnWidths());
481 m_node
->setVersion(props
.m_node
->version());
484 void ViewProperties::setAutoSaveEnabled(bool autoSave
)
486 m_autoSave
= autoSave
;
489 bool ViewProperties::isAutoSaveEnabled() const
494 void ViewProperties::update()
496 m_changedProps
= true;
497 m_node
->setTimestamp(QDateTime::currentDateTime());
500 void ViewProperties::save()
502 qCDebug(DolphinDebug
) << "Saving view-properties to" << m_filePath
;
504 auto cleanDotDirectoryFile
= [this]() {
505 const QString settingsFile
= m_filePath
+ QDir::separator() + ViewPropertiesFileName
;
506 if (QFile::exists(settingsFile
)) {
507 qCDebug(DolphinDebug
) << "cleaning .directory" << settingsFile
;
508 KConfig
cfg(settingsFile
, KConfig::OpenFlag::SimpleConfig
);
509 const auto groupList
= cfg
.groupList();
510 for (const auto &group
: groupList
) {
511 if (group
== QStringLiteral("Dolphin") || group
== QStringLiteral("Settings")) {
512 cfg
.deleteGroup(group
);
515 if (cfg
.groupList().isEmpty()) {
516 QFile::remove(settingsFile
);
517 } else if (cfg
.isDirty()) {
523 // ensures the destination dir exists, in case we don't write metadata directly on the folder
524 QDir
destinationDir(m_filePath
);
525 if (!destinationDir
.exists() && !destinationDir
.mkpath(m_filePath
)) {
526 qCWarning(DolphinDebug
) << "Could not create fake directory to store metadata";
529 KFileMetaData::UserMetaData
metaData(m_filePath
);
530 if (metaData
.isSupported()) {
531 const auto metaDataKey
= QStringLiteral("kde.fm.viewproperties#1");
533 const auto items
= m_node
->items();
534 const auto defaultConfig
= defaultProperties();
535 bool allDefault
= true;
536 for (const auto item
: items
) {
537 if (item
->name() == "Timestamp") {
540 if (item
->name() == "Version") {
541 if (m_node
->version() != CurrentViewPropertiesVersion
) {
548 auto defaultItem
= defaultConfig
->findItem(item
->name());
549 if (!defaultItem
|| defaultItem
->property() != item
->property()) {
556 if (metaData
.hasAttribute(metaDataKey
)) {
557 qCDebug(DolphinDebug
) << "clearing extended attributes for " << m_filePath
;
558 const auto result
= metaData
.setAttribute(metaDataKey
, QString());
559 if (result
!= KFileMetaData::UserMetaData::NoError
) {
560 qCWarning(DolphinDebug
) << "could not clear extended attributes for " << m_filePath
<< "error:" << result
;
563 cleanDotDirectoryFile();
567 // save config to disk
568 if (!m_node
->save()) {
569 qCWarning(DolphinDebug
) << "could not save viewproperties" << m_node
->config()->name();
573 QFile
configFile(m_node
->config()->name());
574 if (!configFile
.open(QIODevice::ReadOnly
)) {
575 qCWarning(DolphinDebug
) << "Could not open readonly config file" << m_node
->config()->name();
577 // load config from disk
578 const QString viewPropertiesString
= configFile
.readAll();
581 const auto result
= metaData
.setAttribute(metaDataKey
, viewPropertiesString
);
582 if (result
!= KFileMetaData::UserMetaData::NoError
) {
583 if (result
== KFileMetaData::UserMetaData::NoSpace
) {
584 // copy settings to dotDirectory file as fallback
585 if (!configFile
.copy(m_filePath
+ QDir::separator() + ViewPropertiesFileName
)) {
586 qCWarning(DolphinDebug
) << "could not write viewproperties to .directory for dir " << m_filePath
;
588 // free the space used by viewproperties from the file metadata
589 metaData
.setAttribute(metaDataKey
, "");
591 qCWarning(DolphinDebug
) << "could not save viewproperties to extended attributes for dir " << m_filePath
<< "error:" << result
;
593 // keep .directory file
596 cleanDotDirectoryFile();
599 m_changedProps
= false;
604 dir
.mkpath(m_filePath
);
605 m_node
->setVersion(CurrentViewPropertiesVersion
);
608 m_changedProps
= false;
611 QString
ViewProperties::destinationDir(const QString
&subDir
) const
613 QString path
= QStandardPaths::writableLocation(QStandardPaths::AppDataLocation
);
614 path
.append("/view_properties/").append(subDir
);
618 QString
ViewProperties::viewModePrefix() const
622 switch (m_node
->viewMode()) {
623 case DolphinView::IconsView
:
624 prefix
= QStringLiteral("Icons_");
626 case DolphinView::CompactView
:
627 prefix
= QStringLiteral("Compact_");
629 case DolphinView::DetailsView
:
630 prefix
= QStringLiteral("Details_");
633 qCWarning(DolphinDebug
) << "Unknown view-mode of the view properties";
639 void ViewProperties::convertAdditionalInfo()
641 QStringList visibleRoles
= m_node
->visibleRoles();
643 const QStringList additionalInfo
= m_node
->additionalInfo();
644 if (!additionalInfo
.isEmpty()) {
645 // Convert the obsolete values like Icons_Size, Details_Date, ...
646 // to Icons_size, Details_date, ... where the suffix just represents
647 // the internal role. One special-case must be handled: "LinkDestination"
648 // has been used for "destination".
649 visibleRoles
.reserve(visibleRoles
.count() + additionalInfo
.count());
650 for (const QString
&info
: additionalInfo
) {
651 QString visibleRole
= info
;
652 int index
= visibleRole
.indexOf('_');
653 if (index
>= 0 && index
+ 1 < visibleRole
.length()) {
655 if (visibleRole
[index
] == QLatin1Char('L')) {
656 visibleRole
.replace(QLatin1String("LinkDestination"), QLatin1String("destination"));
658 visibleRole
[index
] = visibleRole
[index
].toLower();
661 if (!visibleRoles
.contains(visibleRole
)) {
662 visibleRoles
.append(visibleRole
);
667 m_node
->setAdditionalInfo(QStringList());
668 m_node
->setVisibleRoles(visibleRoles
);
669 m_node
->setVersion(AdditionalInfoViewPropertiesVersion
);
673 void ViewProperties::convertNameRoleToTextRole()
675 QStringList visibleRoles
= m_node
->visibleRoles();
676 for (int i
= 0; i
< visibleRoles
.count(); ++i
) {
677 if (visibleRoles
[i
].endsWith(QLatin1String("_name"))) {
678 const int leftLength
= visibleRoles
[i
].length() - 5;
679 visibleRoles
[i
] = visibleRoles
[i
].left(leftLength
) + "_text";
683 QString sortRole
= m_node
->sortRole();
684 if (sortRole
== QLatin1String("name")) {
685 sortRole
= QStringLiteral("text");
688 m_node
->setVisibleRoles(visibleRoles
);
689 m_node
->setSortRole(sortRole
);
690 m_node
->setVersion(NameRolePropertiesVersion
);
694 void ViewProperties::convertDateRoleToModificationTimeRole()
696 QStringList visibleRoles
= m_node
->visibleRoles();
697 for (int i
= 0; i
< visibleRoles
.count(); ++i
) {
698 if (visibleRoles
[i
].endsWith(QLatin1String("_date"))) {
699 const int leftLength
= visibleRoles
[i
].length() - 5;
700 visibleRoles
[i
] = visibleRoles
[i
].left(leftLength
) + "_modificationtime";
704 QString sortRole
= m_node
->sortRole();
705 if (sortRole
== QLatin1String("date")) {
706 sortRole
= QStringLiteral("modificationtime");
709 m_node
->setVisibleRoles(visibleRoles
);
710 m_node
->setSortRole(sortRole
);
711 m_node
->setVersion(DateRolePropertiesVersion
);
715 bool ViewProperties::isPartOfHome(const QString
&filePath
)
717 // For performance reasons cache the path in a static QString
718 // (see QDir::homePath() for more details)
719 static QString homePath
;
720 if (homePath
.isEmpty()) {
721 homePath
= QDir::homePath();
722 Q_ASSERT(!homePath
.isEmpty());
725 return filePath
.startsWith(homePath
);
728 QString
ViewProperties::directoryHashForUrl(const QUrl
&url
)
730 const QByteArray hashValue
= QCryptographicHash::hash(url
.toEncoded(), QCryptographicHash::Sha1
);
731 QString hashString
= hashValue
.toBase64();
732 hashString
.replace('/', '-');