| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at> |
| 3 | SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org> |
| 4 | SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org> |
| 5 | SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer @ kde.org> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 8 | */ |
| 9 | |
| 10 | #include "kcoreurlnavigator.h" |
| 11 | |
| 12 | #include "urlutil_p.h" |
| 13 | |
| 14 | #include <KIO/StatJob> |
| 15 | #include <KLocalizedString> |
| 16 | #include <kfileitem.h> |
| 17 | #include <kprotocolinfo.h> |
| 18 | |
| 19 | #include <QDir> |
| 20 | #include <QMimeData> |
| 21 | #include <QMimeDatabase> |
| 22 | |
| 23 | #include <algorithm> |
| 24 | #include <numeric> |
| 25 | |
| 26 | struct LocationData { |
| 27 | QUrl url; |
| 28 | QVariant state; |
| 29 | }; |
| 30 | |
| 31 | class KCoreUrlNavigatorPrivate |
| 32 | { |
| 33 | public: |
| 34 | KCoreUrlNavigatorPrivate(KCoreUrlNavigator *qq); |
| 35 | |
| 36 | ~KCoreUrlNavigatorPrivate() |
| 37 | { |
| 38 | } |
| 39 | |
| 40 | /* |
| 41 | * Returns true, if the MIME type of the path represents a |
| 42 | * compressed file like TAR or ZIP, as listed in archiveMimetypes |
| 43 | */ |
| 44 | bool isCompressedPath(const QUrl &path, const QStringList &archiveMimetypes) const; |
| 45 | |
| 46 | /* |
| 47 | * Returns the current history index, if historyIndex is |
| 48 | * smaller than 0. If historyIndex is greater or equal than |
| 49 | * the number of available history items, the largest possible |
| 50 | * history index is returned. For the other cases just historyIndex |
| 51 | * is returned. |
| 52 | */ |
| 53 | int adjustedHistoryIndex(int historyIndex) const; |
| 54 | |
| 55 | KCoreUrlNavigator *const q; |
| 56 | |
| 57 | QList<LocationData> m_history; |
| 58 | int m_historyIndex = 0; |
| 59 | }; |
| 60 | |
| 61 | KCoreUrlNavigatorPrivate::KCoreUrlNavigatorPrivate(KCoreUrlNavigator *qq) |
| 62 | : q(qq) |
| 63 | { |
| 64 | } |
| 65 | |
| 66 | bool KCoreUrlNavigatorPrivate::isCompressedPath(const QUrl &url, const QStringList &archiveMimetypes) const |
| 67 | { |
| 68 | QMimeDatabase db; |
| 69 | const QMimeType mime = db.mimeTypeForUrl(url: QUrl(url.toString(options: QUrl::StripTrailingSlash))); |
| 70 | return std::any_of(first: archiveMimetypes.begin(), last: archiveMimetypes.end(), pred: [mime](const QString &archiveType) { |
| 71 | return mime.inherits(mimeTypeName: archiveType); |
| 72 | }); |
| 73 | } |
| 74 | |
| 75 | int KCoreUrlNavigatorPrivate::adjustedHistoryIndex(int historyIndex) const |
| 76 | { |
| 77 | const int historySize = m_history.size(); |
| 78 | if (historyIndex < 0) { |
| 79 | historyIndex = m_historyIndex; |
| 80 | } else if (historyIndex >= historySize) { |
| 81 | historyIndex = historySize - 1; |
| 82 | Q_ASSERT(historyIndex >= 0); // m_history.size() must always be > 0 |
| 83 | } |
| 84 | return historyIndex; |
| 85 | } |
| 86 | |
| 87 | // ------------------------------------------------------------------------------------------------ |
| 88 | |
| 89 | KCoreUrlNavigator::KCoreUrlNavigator(const QUrl &url, QObject *parent) |
| 90 | : QObject(parent) |
| 91 | , d(new KCoreUrlNavigatorPrivate(this)) |
| 92 | { |
| 93 | d->m_history.prepend(t: LocationData{.url: url.adjusted(options: QUrl::NormalizePathSegments), .state: {}}); |
| 94 | } |
| 95 | |
| 96 | KCoreUrlNavigator::~KCoreUrlNavigator() |
| 97 | { |
| 98 | } |
| 99 | |
| 100 | QUrl KCoreUrlNavigator::locationUrl(int historyIndex) const |
| 101 | { |
| 102 | historyIndex = d->adjustedHistoryIndex(historyIndex); |
| 103 | return d->m_history.at(i: historyIndex).url; |
| 104 | } |
| 105 | |
| 106 | void KCoreUrlNavigator::saveLocationState(const QVariant &state) |
| 107 | { |
| 108 | d->m_history[d->m_historyIndex].state = state; |
| 109 | } |
| 110 | |
| 111 | QVariant KCoreUrlNavigator::locationState(int historyIndex) const |
| 112 | { |
| 113 | historyIndex = d->adjustedHistoryIndex(historyIndex); |
| 114 | return d->m_history.at(i: historyIndex).state; |
| 115 | } |
| 116 | |
| 117 | bool KCoreUrlNavigator::goBack() |
| 118 | { |
| 119 | const int count = d->m_history.size(); |
| 120 | if (d->m_historyIndex < count - 1) { |
| 121 | const QUrl newUrl = locationUrl(historyIndex: d->m_historyIndex + 1); |
| 122 | Q_EMIT currentUrlAboutToChange(newUrl); |
| 123 | |
| 124 | ++d->m_historyIndex; |
| 125 | |
| 126 | Q_EMIT historyIndexChanged(); |
| 127 | Q_EMIT historyChanged(); |
| 128 | Q_EMIT currentLocationUrlChanged(); |
| 129 | return true; |
| 130 | } |
| 131 | |
| 132 | return false; |
| 133 | } |
| 134 | |
| 135 | bool KCoreUrlNavigator::goForward() |
| 136 | { |
| 137 | if (d->m_historyIndex > 0) { |
| 138 | const QUrl newUrl = locationUrl(historyIndex: d->m_historyIndex - 1); |
| 139 | Q_EMIT currentUrlAboutToChange(newUrl); |
| 140 | |
| 141 | --d->m_historyIndex; |
| 142 | |
| 143 | Q_EMIT historyIndexChanged(); |
| 144 | Q_EMIT historyChanged(); |
| 145 | Q_EMIT currentLocationUrlChanged(); |
| 146 | return true; |
| 147 | } |
| 148 | |
| 149 | return false; |
| 150 | } |
| 151 | |
| 152 | bool KCoreUrlNavigator::goUp() |
| 153 | { |
| 154 | const QUrl currentUrl = locationUrl(); |
| 155 | const QUrl upUrl = KIO::upUrl(url: currentUrl); |
| 156 | if (!currentUrl.matches(url: upUrl, options: QUrl::StripTrailingSlash)) { |
| 157 | setCurrentLocationUrl(upUrl); |
| 158 | return true; |
| 159 | } |
| 160 | |
| 161 | return false; |
| 162 | } |
| 163 | |
| 164 | QUrl KCoreUrlNavigator::currentLocationUrl() const |
| 165 | { |
| 166 | return locationUrl(); |
| 167 | } |
| 168 | |
| 169 | void KCoreUrlNavigator::setCurrentLocationUrl(const QUrl &newUrl) |
| 170 | { |
| 171 | if (newUrl == locationUrl()) { |
| 172 | return; |
| 173 | } |
| 174 | |
| 175 | QUrl url = newUrl.adjusted(options: QUrl::NormalizePathSegments); |
| 176 | |
| 177 | // This will be used below; we define it here because in the lower part of the |
| 178 | // code locationUrl() and url become the same URLs |
| 179 | QUrl firstChildUrl = KIO::UrlUtil::firstChildUrl(lastUrl: locationUrl(), currentUrl: url); |
| 180 | |
| 181 | const QString scheme = url.scheme(); |
| 182 | if (!scheme.isEmpty()) { |
| 183 | // Check if the URL represents a tar-, zip- or 7z-file, or an archive file supported by krarc. |
| 184 | const QStringList archiveMimetypes = KProtocolInfo::archiveMimetypes(protocol: scheme); |
| 185 | |
| 186 | if (!archiveMimetypes.isEmpty()) { |
| 187 | // Check whether the URL is really part of the archive file, otherwise |
| 188 | // replace it by the local path again. |
| 189 | bool insideCompressedPath = d->isCompressedPath(url, archiveMimetypes); |
| 190 | if (!insideCompressedPath) { |
| 191 | QUrl prevUrl = url; |
| 192 | QUrl parentUrl = KIO::upUrl(url); |
| 193 | while (parentUrl != prevUrl) { |
| 194 | if (d->isCompressedPath(url: parentUrl, archiveMimetypes)) { |
| 195 | insideCompressedPath = true; |
| 196 | break; |
| 197 | } |
| 198 | prevUrl = parentUrl; |
| 199 | parentUrl = KIO::upUrl(url: parentUrl); |
| 200 | } |
| 201 | } |
| 202 | if (!insideCompressedPath) { |
| 203 | // drop the tar:, zip:, sevenz: or krarc: protocol since we are not |
| 204 | // inside the compressed path |
| 205 | url.setScheme(QStringLiteral("file" )); |
| 206 | firstChildUrl.setScheme(QStringLiteral("file" )); |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | // Check whether current history element has the same URL. |
| 212 | // If this is the case, just ignore setting the URL. |
| 213 | const LocationData &data = d->m_history.at(i: d->m_historyIndex); |
| 214 | const bool isUrlEqual = url.matches(url: locationUrl(), options: QUrl::StripTrailingSlash) || (!url.isValid() && url.matches(url: data.url, options: QUrl::StripTrailingSlash)); |
| 215 | if (isUrlEqual) { |
| 216 | return; |
| 217 | } |
| 218 | |
| 219 | Q_EMIT currentUrlAboutToChange(newUrl: url); |
| 220 | |
| 221 | if (d->m_historyIndex > 0) { |
| 222 | // If an URL is set when the history index is not at the end (= 0), |
| 223 | // then clear all previous history elements so that a new history |
| 224 | // tree is started from the current position. |
| 225 | auto begin = d->m_history.begin(); |
| 226 | auto end = begin + d->m_historyIndex; |
| 227 | d->m_history.erase(abegin: begin, aend: end); |
| 228 | d->m_historyIndex = 0; |
| 229 | } |
| 230 | |
| 231 | Q_ASSERT(d->m_historyIndex == 0); |
| 232 | d->m_history.insert(i: 0, t: LocationData{.url: url, .state: {}}); |
| 233 | |
| 234 | // Prevent an endless growing of the history: remembering |
| 235 | // the last 100 Urls should be enough... |
| 236 | const int historyMax = 100; |
| 237 | if (d->m_history.size() > historyMax) { |
| 238 | auto begin = d->m_history.begin() + historyMax; |
| 239 | auto end = d->m_history.end(); |
| 240 | d->m_history.erase(abegin: begin, aend: end); |
| 241 | } |
| 242 | |
| 243 | Q_EMIT historyIndexChanged(); |
| 244 | Q_EMIT historySizeChanged(); |
| 245 | Q_EMIT historyChanged(); |
| 246 | Q_EMIT currentLocationUrlChanged(); |
| 247 | if (firstChildUrl.isValid()) { |
| 248 | Q_EMIT urlSelectionRequested(url: firstChildUrl); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | int KCoreUrlNavigator::historySize() const |
| 253 | { |
| 254 | return d->m_history.count(); |
| 255 | } |
| 256 | |
| 257 | int KCoreUrlNavigator::historyIndex() const |
| 258 | { |
| 259 | return d->m_historyIndex; |
| 260 | } |
| 261 | |
| 262 | #include "moc_kcoreurlnavigator.cpp" |
| 263 | |