diff --git a/kcms/baloo/filteredfoldermodel.cpp b/kcms/baloo/filteredfoldermodel.cpp index 3bb67ff3f..0b28e4640 100644 --- a/kcms/baloo/filteredfoldermodel.cpp +++ b/kcms/baloo/filteredfoldermodel.cpp @@ -1,230 +1,236 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2014 Vishesh Handa * Copyright (C) 2019 Tomaz Canabrava * Copyright (c) 2020 Benjamin Port * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "filteredfoldermodel.h" -#include -#include -#include - #include #include #include #include #include #include #include "baloo/baloosettings.h" namespace { - QStringList addTrailingSlashes(const QStringList& input) { - QStringList output = input; + QString normalizeTrailingSlashes(QString&& input) { + if (!input.endsWith('/')) + return input + QLatin1Char('/'); + return input; + } - for (QString& str : output) { - if (!str.endsWith(QDir::separator())) - str.append(QDir::separator()); + QStringList addTrailingSlashes(QStringList&& list) { + for (QString& str : list) { + str = normalizeTrailingSlashes(std::move(str)); } - - return output; + return list; } QString makeHomePretty(const QString& url) { if (url.startsWith(QDir::homePath())) return QString(url).replace(0, QDir::homePath().length(), QStringLiteral("~")); return url; } - - bool ignoredMountPoint(const QString& mountPoint) { - if (mountPoint == QLatin1String("/")) - return true; - - if (mountPoint.startsWith(QLatin1String("/boot"))) - return true; - - if (mountPoint.startsWith(QLatin1String("/tmp"))) - return true; - - // The user's home directory is forcibly added so we can ignore /home - // if /home actually contains the home directory - return mountPoint.startsWith(QLatin1String("/home")) && - QDir::homePath().startsWith(QLatin1String("/home")); - } } FilteredFolderModel::FilteredFolderModel(BalooSettings *settings, QObject *parent) : QAbstractListModel(parent) , m_settings(settings) { } void FilteredFolderModel::updateDirectoryList() { beginResetModel(); - m_mountPoints.clear(); + const QStringList runtimeExcluded = m_runtimeConfig.excludeFolders(); - QList devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess); + QStringList settingsIncluded = addTrailingSlashes(m_settings->folders()); + QStringList settingsExcluded = addTrailingSlashes(m_settings->excludedFolders()); - for (const Solid::Device& dev : devices) { - const Solid::StorageAccess* sa = dev.as(); - if (!sa->isAccessible()) - continue; + const QString homePath = normalizeTrailingSlashes(QDir::homePath()); - const QString mountPath = sa->filePath(); - if (ignoredMountPoint(mountPath)) - continue; + auto folderListEntry = [&homePath] (const QString& url, bool include, bool fromConfig) + { + QString displayName = url; + if (displayName.size() > 1) { + displayName.chop(1); + } + if (displayName.startsWith(homePath)) { + displayName.replace(0, homePath.length(), QStringLiteral("~/")); + } - m_mountPoints.append(mountPath); - } - m_mountPoints.append(QDir::homePath()); - m_mountPoints = addTrailingSlashes(m_mountPoints); + QString icon = QStringLiteral("folder"); + if (url == homePath) { + icon = QStringLiteral("user-home"); + } else if (!fromConfig) { + icon = QStringLiteral("drive-harddisk"); + } - QStringList includeList = addTrailingSlashes(m_settings->folders()); + return FolderInfo{url, displayName, icon, include, fromConfig}; + }; + m_folderList.clear(); - m_excludeList = addTrailingSlashes(m_settings->excludedFolders()); + for (const QString& folder : settingsIncluded) { + m_folderList.append(folderListEntry(folder, true, true)); + } + for (const QString& folder : settingsExcluded) { + m_folderList.append(folderListEntry(folder, false, true)); + } - // This algorithm seems bogus. verify later. - for (const QString& mountPath : m_mountPoints) { - if (includeList.contains(mountPath)) + // Add any automatically excluded mounts to the list + for (const QString& folder : runtimeExcluded) { + if (settingsIncluded.contains(folder) || + settingsExcluded.contains(folder)) { + // Do not add any duplicates continue; - - if (!m_excludeList.contains(mountPath)) { - m_excludeList.append(mountPath); } + if (m_deletedSettings.contains(folder)) { + // Skip entries deleted from the config + continue; + } + m_folderList.append(folderListEntry(folder, false, false)); } + std::sort(m_folderList.begin(), m_folderList.end(), + [](const FolderInfo& a, const FolderInfo& b) { + return a.url < b.url; + }); + endResetModel(); } QVariant FilteredFolderModel::data(const QModelIndex& idx, int role) const { - if (!idx.isValid() || idx.row() >= m_excludeList.size()) { + if (!idx.isValid() || idx.row() >= m_folderList.size()) { return {}; } - const auto currentUrl = m_excludeList.at(idx.row()); + const auto entry = m_folderList.at(idx.row()); switch (role) { - case Qt::DisplayRole: return folderDisplayName(currentUrl); - case Qt::WhatsThisRole: return currentUrl; - case Qt::DecorationRole: return QIcon::fromTheme(iconName(currentUrl)); - case Qt::ToolTipRole: return makeHomePretty(currentUrl); - case Url: return currentUrl; - case Folder: return folderDisplayName(currentUrl); + case Qt::DisplayRole: return entry.displayName; + case Qt::WhatsThisRole: return entry.url; + case Qt::DecorationRole: return entry.icon; + case Qt::ToolTipRole: return makeHomePretty(entry.url); + case Url: return entry.url; + case Folder: return entry.displayName; + case EnableIndex: return entry.enableIndex; + case Deletable: return entry.isFromConfig; default: return {}; } - - return {}; } +bool FilteredFolderModel::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || idx.row() >= m_folderList.size()) { + return false; + } + FolderInfo& entry = m_folderList[idx.row()]; + if (role == EnableIndex) { + entry.enableIndex = value.toBool(); + syncFolderConfig(entry); + emit dataChanged(idx, idx); + return true; + } + return false; +} + int FilteredFolderModel::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); - return m_excludeList.count(); + return m_folderList.count(); } void FilteredFolderModel::addFolder(const QString& url) { - auto excluded = m_settings->excludedFolders(); - if (excluded.contains(url)) { + QString nUrl = normalizeTrailingSlashes(QUrl(url).toLocalFile()); + + auto it = std::find_if(m_folderList.begin(), m_folderList.end(), + [nUrl](const FolderInfo& folder) { + return folder.url == nUrl; + }); + if (it != m_folderList.end() && (*it).isFromConfig) { return; } - excluded.append(QUrl(url).toLocalFile()); + auto excluded = m_settings->excludedFolders(); + excluded.append(nUrl); std::sort(std::begin(excluded), std::end(excluded)); m_settings->setExcludedFolders(excluded); + m_deletedSettings.removeAll(nUrl); } void FilteredFolderModel::removeFolder(int row) { - auto url = m_excludeList.at(row); - auto excluded = addTrailingSlashes(m_settings->excludedFolders()); - auto included = addTrailingSlashes(m_settings->folders()); - if (excluded.contains(url)) { - excluded.removeAll(url); + auto entry = m_folderList.at(row); + if (!entry.isFromConfig) { + return; + } + if (!entry.enableIndex) { + auto excluded = addTrailingSlashes(m_settings->excludedFolders()); + excluded.removeAll(entry.url); std::sort(std::begin(excluded), std::end(excluded)); m_settings->setExcludedFolders(excluded); - } else if (m_mountPoints.contains(url) && !included.contains(url)) { - included.append(url); + } else { + auto included = addTrailingSlashes(m_settings->folders()); + included.removeAll(entry.url); std::sort(std::begin(included), std::end(included)); m_settings->setFolders(included); } + m_deletedSettings.append(entry.url); } - -QString FilteredFolderModel::folderDisplayName(const QString& url) const +void FilteredFolderModel::syncFolderConfig(const FolderInfo& entry) { - QString name = url; - - // Check Home Dir - QString homePath = QDir::homePath() + QLatin1Char('/'); - if (url == homePath) { - return QDir(homePath).dirName(); - } - - if (url.startsWith(homePath)) { - name = url.mid(homePath.size()); - } - else { - // Check Mount allMountPointsExcluded - for (QString mountPoint : m_mountPoints) { - if (url.startsWith(mountPoint)) { - name = QLatin1Char('[') + mountPoint+ QLatin1String("]/") + url.mid(mountPoint.length()); - break; - } + auto excluded = addTrailingSlashes(m_settings->excludedFolders()); + auto included = addTrailingSlashes(m_settings->folders()); + if (entry.enableIndex) { + included.append(entry.url); + std::sort(std::begin(included), std::end(included)); + if (excluded.removeAll(entry.url)) { + std::sort(std::begin(excluded), std::end(excluded)); + m_settings->setExcludedFolders(excluded); } + m_settings->setFolders(included); + } else { + excluded.append(entry.url); + std::sort(std::begin(excluded), std::end(excluded)); + if (included.removeAll(entry.url)) { + std::sort(std::begin(included), std::end(included)); + m_settings->setFolders(included); + } + m_settings->setExcludedFolders(excluded); } - - if (name.endsWith(QLatin1Char('/'))) { - name = name.mid(0, name.size() - 1); - } - return name; } QHash FilteredFolderModel::roleNames() const { return { {Url, "url"}, {Folder, "folder"}, + {EnableIndex, "enableIndex"}, + {Deletable, "deletable"}, {Qt::DecorationRole, "decoration"} }; } -QString FilteredFolderModel::iconName(QString path) const -{ - // Ensure paths end with / - if (!path.endsWith(QDir::separator())) - path.append(QDir::separator()); - - QString homePath = QDir::homePath(); - if (!homePath.endsWith(QDir::separator())) - homePath.append(QDir::separator()); - - if (path == homePath) - return QStringLiteral("user-home"); - - if (m_mountPoints.contains(path)) - return QStringLiteral("drive-harddisk"); - - return QStringLiteral("folder"); -} - diff --git a/kcms/baloo/filteredfoldermodel.h b/kcms/baloo/filteredfoldermodel.h index b294ca7c1..e0eb18c6f 100644 --- a/kcms/baloo/filteredfoldermodel.h +++ b/kcms/baloo/filteredfoldermodel.h @@ -1,66 +1,72 @@ /* * This file is part of the KDE Baloo project * Copyright (C) 2014 Vishesh Handa * Copyright (c) 2020 Benjamin Port * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #ifndef FILTEREDFOLDERMODEL_H #define FILTEREDFOLDERMODEL_H #include +#include class BalooSettings; class FilteredFolderModel : public QAbstractListModel { Q_OBJECT public: explicit FilteredFolderModel(BalooSettings *settings, QObject *parent); enum Roles { Folder = Qt::UserRole + 1, - Url + Url, + EnableIndex, + Deletable, }; QVariant data(const QModelIndex& idx, int role) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; int rowCount(const QModelIndex& parent) const override; Q_INVOKABLE void addFolder(const QString& folder); Q_INVOKABLE void removeFolder(int row); QHash roleNames() const override; public slots: void updateDirectoryList(); private: - QString folderDisplayName(const QString& url) const; + BalooSettings *m_settings; + Baloo::IndexerConfig m_runtimeConfig; + + struct FolderInfo { + QString url; + QString displayName; + QString icon; + bool enableIndex; + bool isFromConfig; + }; - /** - * @brief Get the theme valid icon name for \p path. - * - * @param path Path to be analysed. - * @return One of: "user-home", "drive-harddisk" or "folder" - */ - QString iconName(QString path) const; + QVector m_folderList; + QStringList m_deletedSettings; //< track deleted entries - BalooSettings *m_settings; - QStringList m_mountPoints; - QStringList m_excludeList; + void syncFolderConfig(const FolderInfo& entry); }; #endif // FILTEREDFOLDERMODEL_H diff --git a/kcms/baloo/package/contents/ui/main.qml b/kcms/baloo/package/contents/ui/main.qml index 493c639a8..b252339dc 100644 --- a/kcms/baloo/package/contents/ui/main.qml +++ b/kcms/baloo/package/contents/ui/main.qml @@ -1,141 +1,198 @@ /* * Copyright 2018 Tomaz Canabrava * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.11 as QQC2 import QtQuick.Dialogs 1.2 as QtDialogs import org.kde.kirigami 2.4 as Kirigami import org.kde.kcm 1.1 as KCM KCM.SimpleKCM { id: root implicitHeight: Kirigami.Units.gridUnit * 22 KCM.ConfigModule.quickHelp: i18n("This module lets you configure the file indexer and search functionality.") ColumnLayout { anchors.fill: parent anchors.margins: Kirigami.Units.largeSpacing QQC2.Label { text: i18n("File Search helps you quickly locate all your files based on their content.") } QQC2.CheckBox { id: fileSearchEnabled text: i18n("Enable File Search") enabled: !kcm.balooSettings.isImmutable("indexingEnabled") checked: kcm.balooSettings.indexingEnabled onCheckStateChanged: { kcm.balooSettings.indexingEnabled = checked } } RowLayout { Layout.fillWidth: true Item { width: units.largeSpacing } ColumnLayout { QQC2.CheckBox { id: indexFileContents text: i18n("Also index file content") enabled: fileSearchEnabled.checked && !kcm.balooSettings.isImmutable("onlyBasicIndexing") checked: !kcm.balooSettings.onlyBasicIndexing onCheckStateChanged: kcm.balooSettings.onlyBasicIndexing = !checked } QQC2.CheckBox { id: indexHiddenFolders text: i18n("Index hidden files and folders") enabled: fileSearchEnabled.checked && !kcm.balooSettings.isImmutable("indexHiddenFolders") checked: kcm.balooSettings.indexHiddenFolders onCheckStateChanged: kcm.balooSettings.indexHiddenFolders = checked } } } Item { Layout.preferredHeight: Kirigami.Units.gridUnit } QQC2.Label { - text: i18n("Do not search in these locations:") + text: i18n("Folder specific configuration:") } QQC2.ScrollView { id: bgObject Component.onCompleted: bgObject.background.visible = true Layout.fillWidth: true Layout.fillHeight: true ListView { - id: fileExcludeList - + id: directoryConfigList clip: true + currentIndex: -1 + model: kcm.filteredModel - delegate: Kirigami.BasicListItem { - icon: model.decoration - label: model.folder - onClicked: fileExcludeList.currentIndex = index - } + delegate: directoryConfigDelegate } } - RowLayout { - QQC2.Button { - id: addFolder - icon.name: "list-add" - onClicked: fileDialogLoader.active = true + QQC2.Button { + Layout.alignment: Qt.AlignRight + id: addFolder + icon.name: "folder-add" + text: i18n("Add folder configuration...") + onClicked: fileDialogLoader.active = true + } + } + + Component { + id: directoryConfigDelegate + Kirigami.SwipeListItem { + id: listItem + onClicked: { + directoryConfigList.currentIndex = index } + property int iconSize: Kirigami.Units.iconSizes.smallMedium + property bool selected: directoryConfigList.currentIndex === index - QQC2.Button{ - id: removeFolder - icon.name: "list-remove" - enabled: fileExcludeList.currentIndex !== -1 - onClicked: { - kcm.filteredModel.removeFolder(fileExcludeList.currentIndex) + RowLayout { + spacing: units.smallSpacing + + Kirigami.Icon { + source: model.enableIndex ? "search" : "list-remove" + height: listItem.iconSize + width: listItem.iconSize + } + + ColumnLayout { + RowLayout { + spacing: units.smallSpacing + + Kirigami.Icon { + source: model.decoration + height: listItem.iconSize + width: listItem.iconSize + } + QQC2.Label { + text: model.folder + elide: Text.ElideRight + Layout.fillWidth: true + } + } + QQC2.Label { + text: (model.enableIndex ? i18n("%1 is included.", model.url) + : i18n("%1 is excluded.", model.url)) + elide: Text.ElideRight + Layout.fillWidth: true + opacity: listItem.hovered ? 0.8 : 0.6 + visible: listItem.selected + } + } + + QQC2.ToolButton { + visible: listItem.hovered && listItem.actionsVisible + height: listItem.iconSize + icon.name: "search" + text: model.enableIndex ? i18n("Disable indexing") : i18n("Enable indexing") + onClicked: { + model.enableIndex = !model.enableIndex + } } } + + actions: [ + Kirigami.Action { + id: removeFolder + enabled: model.deletable + icon.name: "user-trash" + tooltip: i18n("Delete entry") + onTriggered: { + kcm.filteredModel.removeFolder(index) + } + } + ] } } Loader { id: fileDialogLoader active: false sourceComponent: QtDialogs.FileDialog { title: i18n("Select a folder to filter") folder: shortcuts.home selectFolder: true onAccepted: { kcm.filteredModel.addFolder(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } Component.onCompleted: open() } } }