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 QTemporaryFile tempFile
;
46 tempFile
.setAutoRemove(false);
47 if (!tempFile
.open()) {
48 qCWarning(DolphinDebug
) << "Could not open temp file";
52 if (QFile::exists(settingsFile
)) {
53 // copy settings to tempfile to load them separately
54 QFile::remove(tempFile
.fileName());
55 QFile::copy(settingsFile
, tempFile
.fileName());
57 auto config
= KConfig(tempFile
.fileName(), KConfig::SimpleConfig
);
58 // ignore settings that are outside of dolphin scope
59 if (config
.hasGroup("Dolphin") || config
.hasGroup("Settings")) {
60 const auto groupList
= config
.groupList();
61 for (const auto &group
: groupList
) {
62 if (group
!= QStringLiteral("Dolphin") && group
!= QStringLiteral("Settings")) {
63 config
.deleteGroup(group
);
66 return new ViewPropertySettings(KSharedConfig::openConfig(tempFile
.fileName(), KConfig::SimpleConfig
));
68 } else if (!config
.groupList().isEmpty()) {
69 // clear temp file content
70 QFile::remove(tempFile
.fileName());
75 const QString viewPropertiesString
= metadata
.attribute(QStringLiteral("kde.fm.viewproperties#1"));
76 if (!viewPropertiesString
.isEmpty()) {
77 // load view properties from xattr to temp file then loads into ViewPropertySettings
78 // clear the temp file
79 QFile
outputFile(tempFile
.fileName());
80 outputFile
.open(QIODevice::WriteOnly
);
81 outputFile
.write(viewPropertiesString
.toUtf8());
84 return new ViewPropertySettings(KSharedConfig::openConfig(tempFile
.fileName(), KConfig::SimpleConfig
));
87 ViewPropertySettings
*ViewProperties::defaultProperties() const
89 auto props
= loadProperties(destinationDir(QStringLiteral("global")));
90 if (props
== nullptr) {
91 qCWarning(DolphinDebug
) << "Could not load default global viewproperties";
92 props
= new ViewPropertySettings
;
98 ViewProperties::ViewProperties(const QUrl
&url
)
99 : m_changedProps(false)
103 GeneralSettings
*settings
= GeneralSettings::self();
104 const bool useGlobalViewProps
= settings
->globalViewProps() || url
.isEmpty();
105 bool useSearchView
= false;
106 bool useTrashView
= false;
107 bool useRecentDocumentsView
= false;
108 bool useDownloadsView
= false;
110 // We try and save it to the file .directory in the directory being viewed.
111 // If the directory is not writable by the user or the directory is not local,
112 // we store the properties information in a local file.
113 if (url
.scheme().contains(QLatin1String("search"))) {
114 m_filePath
= destinationDir(QStringLiteral("search/")) + directoryHashForUrl(url
);
115 useSearchView
= true;
116 } else if (url
.scheme() == QLatin1String("trash")) {
117 m_filePath
= destinationDir(QStringLiteral("trash"));
119 } else if (url
.scheme() == QLatin1String("recentlyused")) {
120 m_filePath
= destinationDir(QStringLiteral("recentlyused"));
121 useRecentDocumentsView
= true;
122 } else if (url
.scheme() == QLatin1String("timeline")) {
123 m_filePath
= destinationDir(QStringLiteral("timeline"));
124 useRecentDocumentsView
= true;
125 } else if (useGlobalViewProps
) {
126 m_filePath
= destinationDir(QStringLiteral("global"));
127 } else if (url
.isLocalFile()) {
128 m_filePath
= url
.toLocalFile();
130 bool useDestinationDir
= !isPartOfHome(m_filePath
);
131 if (!useDestinationDir
) {
132 const KFileItem
fileItem(url
);
133 useDestinationDir
= fileItem
.isSlow();
136 if (!useDestinationDir
) {
137 const QFileInfo
dirInfo(m_filePath
);
138 const QFileInfo
fileInfo(m_filePath
+ QDir::separator() + ViewPropertiesFileName
);
139 useDestinationDir
= !dirInfo
.isWritable() || (dirInfo
.size() > 0 && fileInfo
.exists() && !(fileInfo
.isReadable() && fileInfo
.isWritable()));
142 if (useDestinationDir
) {
144 // m_filePath probably begins with C:/ - the colon is not a valid character for paths though
145 m_filePath
= QDir::separator() + m_filePath
.remove(QLatin1Char(':'));
147 m_filePath
= destinationDir(QStringLiteral("local")) + m_filePath
;
150 if (m_filePath
== QStandardPaths::writableLocation(QStandardPaths::DownloadLocation
)) {
151 useDownloadsView
= true;
154 m_filePath
= destinationDir(QStringLiteral("remote")) + m_filePath
;
157 m_node
= loadProperties(m_filePath
);
159 bool useDefaultSettings
= useGlobalViewProps
||
160 // If the props timestamp is too old,
161 // use default values instead.
162 (m_node
!= nullptr && (!useGlobalViewProps
|| useSearchView
|| useTrashView
|| useRecentDocumentsView
|| useDownloadsView
)
163 && m_node
->timestamp() < settings
->viewPropsTimestamp());
165 if (m_node
== nullptr) {
166 // no settings found for m_filepath, load defaults
167 m_node
= defaultProperties();
168 useDefaultSettings
= true;
171 // default values for special directories
172 if (useDefaultSettings
) {
174 const QString path
= url
.path();
176 if (path
== QLatin1String("/images")) {
177 setViewMode(DolphinView::IconsView
);
178 setPreviewsShown(true);
179 setVisibleRoles({"text", "dimensions", "imageDateTime"});
180 } else if (path
== QLatin1String("/audio")) {
181 setViewMode(DolphinView::DetailsView
);
182 setVisibleRoles({"text", "artist", "album", "duration"});
183 } else if (path
== QLatin1String("/videos")) {
184 setViewMode(DolphinView::IconsView
);
185 setPreviewsShown(true);
186 setVisibleRoles({"text"});
188 setViewMode(DolphinView::DetailsView
);
189 setVisibleRoles({"text", "path", "modificationtime"});
191 } else if (useTrashView
) {
192 setViewMode(DolphinView::DetailsView
);
193 setVisibleRoles({"text", "path", "deletiontime"});
194 } else if (useRecentDocumentsView
|| useDownloadsView
) {
195 setSortOrder(Qt::DescendingOrder
);
196 setSortFoldersFirst(false);
197 setGroupedSorting(true);
199 if (useRecentDocumentsView
) {
200 setSortRole(QByteArrayLiteral("accesstime"));
201 setViewMode(DolphinView::DetailsView
);
202 setVisibleRoles({"text", "path", "accesstime"});
204 setSortRole(QByteArrayLiteral("modificationtime"));
207 m_changedProps
= false;
211 if (m_node
->version() < CurrentViewPropertiesVersion
) {
212 // The view-properties have an outdated version. Convert the properties
213 // to the changes of the current version.
214 if (m_node
->version() < AdditionalInfoViewPropertiesVersion
) {
215 convertAdditionalInfo();
216 Q_ASSERT(m_node
->version() == AdditionalInfoViewPropertiesVersion
);
219 if (m_node
->version() < NameRolePropertiesVersion
) {
220 convertNameRoleToTextRole();
221 Q_ASSERT(m_node
->version() == NameRolePropertiesVersion
);
224 if (m_node
->version() < DateRolePropertiesVersion
) {
225 convertDateRoleToModificationTimeRole();
226 Q_ASSERT(m_node
->version() == DateRolePropertiesVersion
);
229 m_node
->setVersion(CurrentViewPropertiesVersion
);
233 ViewProperties::~ViewProperties()
235 if (m_changedProps
&& m_autoSave
) {
239 if (!m_node
->config()->name().endsWith(ViewPropertiesFileName
)) {
241 QFile::remove(m_node
->config()->name());
248 void ViewProperties::setViewMode(DolphinView::Mode mode
)
250 if (m_node
->viewMode() != mode
) {
251 m_node
->setViewMode(mode
);
256 DolphinView::Mode
ViewProperties::viewMode() const
258 const int mode
= qBound(0, m_node
->viewMode(), 2);
259 return static_cast<DolphinView::Mode
>(mode
);
262 void ViewProperties::setPreviewsShown(bool show
)
264 if (m_node
->previewsShown() != show
) {
265 m_node
->setPreviewsShown(show
);
270 bool ViewProperties::previewsShown() const
272 return m_node
->previewsShown();
275 void ViewProperties::setHiddenFilesShown(bool show
)
277 if (m_node
->hiddenFilesShown() != show
) {
278 m_node
->setHiddenFilesShown(show
);
283 void ViewProperties::setGroupedSorting(bool grouped
)
285 if (m_node
->groupedSorting() != grouped
) {
286 m_node
->setGroupedSorting(grouped
);
291 bool ViewProperties::groupedSorting() const
293 return m_node
->groupedSorting();
296 bool ViewProperties::hiddenFilesShown() const
298 return m_node
->hiddenFilesShown();
301 void ViewProperties::setSortRole(const QByteArray
&role
)
303 if (m_node
->sortRole() != role
) {
304 m_node
->setSortRole(role
);
309 QByteArray
ViewProperties::sortRole() const
311 return m_node
->sortRole().toLatin1();
314 void ViewProperties::setSortOrder(Qt::SortOrder sortOrder
)
316 if (m_node
->sortOrder() != sortOrder
) {
317 m_node
->setSortOrder(sortOrder
);
322 Qt::SortOrder
ViewProperties::sortOrder() const
324 return static_cast<Qt::SortOrder
>(m_node
->sortOrder());
327 void ViewProperties::setSortFoldersFirst(bool foldersFirst
)
329 if (m_node
->sortFoldersFirst() != foldersFirst
) {
330 m_node
->setSortFoldersFirst(foldersFirst
);
335 bool ViewProperties::sortFoldersFirst() const
337 return m_node
->sortFoldersFirst();
340 void ViewProperties::setSortHiddenLast(bool hiddenLast
)
342 if (m_node
->sortHiddenLast() != hiddenLast
) {
343 m_node
->setSortHiddenLast(hiddenLast
);
348 bool ViewProperties::sortHiddenLast() const
350 return m_node
->sortHiddenLast();
353 void ViewProperties::setVisibleRoles(const QList
<QByteArray
> &roles
)
355 if (roles
== visibleRoles()) {
359 // See ViewProperties::visibleRoles() for the storage format
360 // of the additional information.
362 // Remove the old values stored for the current view-mode
363 const QStringList oldVisibleRoles
= m_node
->visibleRoles();
364 const QString prefix
= viewModePrefix();
365 QStringList newVisibleRoles
= oldVisibleRoles
;
366 for (int i
= newVisibleRoles
.count() - 1; i
>= 0; --i
) {
367 if (newVisibleRoles
[i
].startsWith(prefix
)) {
368 newVisibleRoles
.removeAt(i
);
372 // Add the updated values for the current view-mode
373 newVisibleRoles
.reserve(roles
.count());
374 for (const QByteArray
&role
: roles
) {
375 newVisibleRoles
.append(prefix
+ role
);
378 if (oldVisibleRoles
!= newVisibleRoles
) {
379 const bool markCustomizedDetails
= (m_node
->viewMode() == DolphinView::DetailsView
) && !newVisibleRoles
.contains(CustomizedDetailsString
);
380 if (markCustomizedDetails
) {
381 // The additional information of the details-view has been modified. Set a marker,
382 // so that it is allowed to also show no additional information without doing the
383 // fallback to show the size and date per default.
384 newVisibleRoles
.append(CustomizedDetailsString
);
387 m_node
->setVisibleRoles(newVisibleRoles
);
392 QList
<QByteArray
> ViewProperties::visibleRoles() const
394 // The shown additional information is stored for each view-mode separately as
395 // string with the view-mode as prefix. Example:
397 // AdditionalInfo=Details_size,Details_date,Details_owner,Icons_size
399 // To get the representation as QList<QByteArray>, the current
400 // view-mode must be checked and the values of this mode added to the list.
402 // For the details-view a special case must be respected: Per default the size
403 // and date should be shown without creating a .directory file. Only if
404 // the user explicitly has modified the properties of the details view (marked
405 // by "CustomizedDetails"), also a details-view with no additional information
408 QList
<QByteArray
> roles
{"text"};
410 // Iterate through all stored keys and append all roles that match to
411 // the current view mode.
412 const QString prefix
= viewModePrefix();
413 const int prefixLength
= prefix
.length();
415 const QStringList visibleRoles
= m_node
->visibleRoles();
416 for (const QString
&visibleRole
: visibleRoles
) {
417 if (visibleRole
.startsWith(prefix
)) {
418 const QByteArray role
= visibleRole
.right(visibleRole
.length() - prefixLength
).toLatin1();
419 if (role
!= "text") {
425 // For the details view the size and date should be shown per default
426 // until the additional information has been explicitly changed by the user
427 const bool useDefaultValues
= roles
.count() == 1 // "text"
428 && (m_node
->viewMode() == DolphinView::DetailsView
) && !visibleRoles
.contains(CustomizedDetailsString
);
429 if (useDefaultValues
) {
430 roles
.append("size");
431 roles
.append("modificationtime");
437 void ViewProperties::setHeaderColumnWidths(const QList
<int> &widths
)
439 if (m_node
->headerColumnWidths() != widths
) {
440 m_node
->setHeaderColumnWidths(widths
);
445 QList
<int> ViewProperties::headerColumnWidths() const
447 return m_node
->headerColumnWidths();
450 void ViewProperties::setDirProperties(const ViewProperties
&props
)
452 setViewMode(props
.viewMode());
453 setPreviewsShown(props
.previewsShown());
454 setHiddenFilesShown(props
.hiddenFilesShown());
455 setGroupedSorting(props
.groupedSorting());
456 setSortRole(props
.sortRole());
457 setSortOrder(props
.sortOrder());
458 setSortFoldersFirst(props
.sortFoldersFirst());
459 setSortHiddenLast(props
.sortHiddenLast());
460 setVisibleRoles(props
.visibleRoles());
461 setHeaderColumnWidths(props
.headerColumnWidths());
462 m_node
->setVersion(props
.m_node
->version());
465 void ViewProperties::setAutoSaveEnabled(bool autoSave
)
467 m_autoSave
= autoSave
;
470 bool ViewProperties::isAutoSaveEnabled() const
475 void ViewProperties::update()
477 m_changedProps
= true;
478 m_node
->setTimestamp(QDateTime::currentDateTime());
481 void ViewProperties::save()
483 qCDebug(DolphinDebug
) << "Saving view-properties to" << m_filePath
;
485 auto cleanDotDirectoryFile
= [this]() {
486 const QString settingsFile
= m_filePath
+ QDir::separator() + ViewPropertiesFileName
;
487 if (QFile::exists(settingsFile
)) {
488 qCDebug(DolphinDebug
) << "cleaning .directory" << settingsFile
;
489 KConfig
cfg(settingsFile
, KConfig::OpenFlag::SimpleConfig
);
490 const auto groupList
= cfg
.groupList();
491 for (const auto &group
: groupList
) {
492 if (group
== QStringLiteral("Dolphin") || group
== QStringLiteral("Settings")) {
493 cfg
.deleteGroup(group
);
496 if (cfg
.groupList().isEmpty()) {
497 QFile::remove(settingsFile
);
498 } else if (cfg
.isDirty()) {
504 // ensures the destination dir exists, in case we don't write metadata directly on the folder
505 QDir
destinationDir(m_filePath
);
506 if (!destinationDir
.exists() && !destinationDir
.mkpath(m_filePath
)) {
507 qCWarning(DolphinDebug
) << "Could not create fake directory to store metadata";
510 KFileMetaData::UserMetaData
metaData(m_filePath
);
511 if (metaData
.isSupported()) {
512 const auto metaDataKey
= QStringLiteral("kde.fm.viewproperties#1");
514 const auto items
= m_node
->items();
515 const bool allDefault
= std::all_of(items
.cbegin(), items
.cend(), [this](const KConfigSkeletonItem
*item
) {
516 return item
->name() == "Timestamp" || (item
->name() == "Version" && m_node
->version() == CurrentViewPropertiesVersion
) || item
->isDefault();
519 if (metaData
.hasAttribute(metaDataKey
)) {
520 qCDebug(DolphinDebug
) << "clearing extended attributes for " << m_filePath
;
521 const auto result
= metaData
.setAttribute(metaDataKey
, QString());
522 if (result
!= KFileMetaData::UserMetaData::NoError
) {
523 qCWarning(DolphinDebug
) << "could not clear extended attributes for " << m_filePath
<< "error:" << result
;
526 cleanDotDirectoryFile();
530 // save config to disk
531 if (!m_node
->save()) {
532 qCWarning(DolphinDebug
) << "could not save viewproperties" << m_node
->config()->name();
536 QFile
configFile(m_node
->config()->name());
537 if (!configFile
.open(QIODevice::ReadOnly
)) {
538 qCWarning(DolphinDebug
) << "Could not open readonly config file" << m_node
->config()->name();
540 // load config from disk
541 const QString viewPropertiesString
= configFile
.readAll();
544 const auto result
= metaData
.setAttribute(metaDataKey
, viewPropertiesString
);
545 if (result
!= KFileMetaData::UserMetaData::NoError
) {
546 if (result
== KFileMetaData::UserMetaData::NoSpace
) {
547 // copy settings to dotDirectory file as fallback
548 if (!configFile
.copy(m_filePath
+ QDir::separator() + ViewPropertiesFileName
)) {
549 qCWarning(DolphinDebug
) << "could not write viewproperties to .directory for dir " << m_filePath
;
551 // free the space used by viewproperties from the file metadata
552 metaData
.setAttribute(metaDataKey
, "");
554 qCWarning(DolphinDebug
) << "could not save viewproperties to extended attributes for dir " << m_filePath
<< "error:" << result
;
556 // keep .directory file
559 cleanDotDirectoryFile();
562 m_changedProps
= false;
567 dir
.mkpath(m_filePath
);
568 m_node
->setVersion(CurrentViewPropertiesVersion
);
571 m_changedProps
= false;
574 QString
ViewProperties::destinationDir(const QString
&subDir
) const
576 QString path
= QStandardPaths::writableLocation(QStandardPaths::AppDataLocation
);
577 path
.append("/view_properties/").append(subDir
);
581 QString
ViewProperties::viewModePrefix() const
585 switch (m_node
->viewMode()) {
586 case DolphinView::IconsView
:
587 prefix
= QStringLiteral("Icons_");
589 case DolphinView::CompactView
:
590 prefix
= QStringLiteral("Compact_");
592 case DolphinView::DetailsView
:
593 prefix
= QStringLiteral("Details_");
596 qCWarning(DolphinDebug
) << "Unknown view-mode of the view properties";
602 void ViewProperties::convertAdditionalInfo()
604 QStringList visibleRoles
= m_node
->visibleRoles();
606 const QStringList additionalInfo
= m_node
->additionalInfo();
607 if (!additionalInfo
.isEmpty()) {
608 // Convert the obsolete values like Icons_Size, Details_Date, ...
609 // to Icons_size, Details_date, ... where the suffix just represents
610 // the internal role. One special-case must be handled: "LinkDestination"
611 // has been used for "destination".
612 visibleRoles
.reserve(visibleRoles
.count() + additionalInfo
.count());
613 for (const QString
&info
: additionalInfo
) {
614 QString visibleRole
= info
;
615 int index
= visibleRole
.indexOf('_');
616 if (index
>= 0 && index
+ 1 < visibleRole
.length()) {
618 if (visibleRole
[index
] == QLatin1Char('L')) {
619 visibleRole
.replace(QLatin1String("LinkDestination"), QLatin1String("destination"));
621 visibleRole
[index
] = visibleRole
[index
].toLower();
624 if (!visibleRoles
.contains(visibleRole
)) {
625 visibleRoles
.append(visibleRole
);
630 m_node
->setAdditionalInfo(QStringList());
631 m_node
->setVisibleRoles(visibleRoles
);
632 m_node
->setVersion(AdditionalInfoViewPropertiesVersion
);
636 void ViewProperties::convertNameRoleToTextRole()
638 QStringList visibleRoles
= m_node
->visibleRoles();
639 for (int i
= 0; i
< visibleRoles
.count(); ++i
) {
640 if (visibleRoles
[i
].endsWith(QLatin1String("_name"))) {
641 const int leftLength
= visibleRoles
[i
].length() - 5;
642 visibleRoles
[i
] = visibleRoles
[i
].left(leftLength
) + "_text";
646 QString sortRole
= m_node
->sortRole();
647 if (sortRole
== QLatin1String("name")) {
648 sortRole
= QStringLiteral("text");
651 m_node
->setVisibleRoles(visibleRoles
);
652 m_node
->setSortRole(sortRole
);
653 m_node
->setVersion(NameRolePropertiesVersion
);
657 void ViewProperties::convertDateRoleToModificationTimeRole()
659 QStringList visibleRoles
= m_node
->visibleRoles();
660 for (int i
= 0; i
< visibleRoles
.count(); ++i
) {
661 if (visibleRoles
[i
].endsWith(QLatin1String("_date"))) {
662 const int leftLength
= visibleRoles
[i
].length() - 5;
663 visibleRoles
[i
] = visibleRoles
[i
].left(leftLength
) + "_modificationtime";
667 QString sortRole
= m_node
->sortRole();
668 if (sortRole
== QLatin1String("date")) {
669 sortRole
= QStringLiteral("modificationtime");
672 m_node
->setVisibleRoles(visibleRoles
);
673 m_node
->setSortRole(sortRole
);
674 m_node
->setVersion(DateRolePropertiesVersion
);
678 bool ViewProperties::isPartOfHome(const QString
&filePath
)
680 // For performance reasons cache the path in a static QString
681 // (see QDir::homePath() for more details)
682 static QString homePath
;
683 if (homePath
.isEmpty()) {
684 homePath
= QDir::homePath();
685 Q_ASSERT(!homePath
.isEmpty());
688 return filePath
.startsWith(homePath
);
691 QString
ViewProperties::directoryHashForUrl(const QUrl
&url
)
693 const QByteArray hashValue
= QCryptographicHash::hash(url
.toEncoded(), QCryptographicHash::Sha1
);
694 QString hashString
= hashValue
.toBase64();
695 hashString
.replace('/', '-');