File indexing completed on 2024-04-28 05:49:18

0001 /* This file is part of the KDE project
0002 
0003    SPDX-FileCopyrightText: 2018 Gregor Mi <codestruct@posteo.org>
0004    SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org>
0005 
0006    SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "tabswitcherfilesmodel.h"
0010 
0011 #include <QBrush>
0012 #include <QFileInfo>
0013 #include <QMimeDatabase>
0014 
0015 #include <KTextEditor/Document>
0016 
0017 #include <algorithm>
0018 
0019 namespace detail
0020 {
0021 FilenameListItem::FilenameListItem(DocOrWidget doc)
0022     : document(doc)
0023 {
0024 }
0025 
0026 QIcon FilenameListItem::icon() const
0027 {
0028     if (auto document = this->document.doc()) {
0029         return QIcon::fromTheme(QMimeDatabase().mimeTypeForUrl(document->url()).iconName());
0030     } else if (auto widget = this->document.widget()) {
0031         return widget->windowIcon();
0032     }
0033     return {};
0034 }
0035 
0036 QString FilenameListItem::documentName() const
0037 {
0038     return document.doc() ? document.doc()->documentName() : document.widget()->windowTitle();
0039 }
0040 
0041 QString FilenameListItem::fullPath() const
0042 {
0043     return document.doc() ? document.doc()->url().toLocalFile() : QString();
0044 }
0045 
0046 /**
0047  * Note that if strs contains less than 2 items, the result will be an empty string.
0048  */
0049 QString longestCommonPrefix(std::vector<QString> const &strs)
0050 {
0051     // only 2 or more items can have a common prefix
0052     if (strs.size() < 2) {
0053         return QString();
0054     }
0055 
0056     // get the min length
0057     auto it = std::min_element(strs.begin(), strs.end(), [](const QString &lhs, const QString &rhs) {
0058         return lhs.size() < rhs.size();
0059     });
0060     const int n = it->size();
0061 
0062     for (int pos = 0; pos < n; pos++) { // check each character
0063         for (size_t i = 1; i < strs.size(); i++) {
0064             if (strs[i][pos] != strs[i - 1][pos]) { // we found a mis-match
0065                 // reverse search to find path separator
0066                 const int sepIndex = QStringView(strs.front()).left(pos).lastIndexOf(QLatin1Char('/'));
0067                 if (sepIndex >= 0) {
0068                     pos = sepIndex + 1;
0069                 }
0070                 return strs.front().left(pos);
0071             }
0072         }
0073     }
0074     // prefix is n-length
0075     return strs.front().left(n);
0076 }
0077 
0078 void post_process(FilenameList &data)
0079 {
0080     // collect non-empty paths
0081     std::vector<QString> paths;
0082     for (const auto &item : data) {
0083         const auto path = item.fullPath();
0084         if (!path.isEmpty()) {
0085             paths.push_back(path);
0086         }
0087     }
0088 
0089     const QString prefix = longestCommonPrefix(paths);
0090     int prefix_length = prefix.length();
0091     if (prefix_length == 1) { // if there is only the "/" at the beginning, then keep it
0092         prefix_length = 0;
0093     }
0094 
0095     for (auto &item : data) {
0096         // Note that item.documentName can contain additional characters - e.g. "README.md (2)" -
0097         // so we cannot use that and have to parse the base filename by other means:
0098         const QString basename = QFileInfo(item.fullPath()).fileName(); // e.g. "archive.tar.gz"
0099 
0100         // cut prefix (left side) and cut document name (plus slash) on the right side
0101         int len = item.fullPath().length() - prefix_length - basename.length() - 1;
0102         if (len > 0) { // only assign in case item.fullPath() is not empty
0103             // "PREFIXPATH/REMAININGPATH/BASENAME" --> "REMAININGPATH"
0104             item.displayPathPrefix = item.fullPath().mid(prefix_length, len);
0105         } else {
0106             item.displayPathPrefix.clear();
0107         }
0108     }
0109 }
0110 }
0111 
0112 detail::TabswitcherFilesModel::TabswitcherFilesModel(QObject *parent)
0113     : QAbstractTableModel(parent)
0114 {
0115 }
0116 
0117 bool detail::TabswitcherFilesModel::insertDocument(int row, DocOrWidget document)
0118 {
0119     beginInsertRows(QModelIndex(), row, row);
0120     data_.insert(data_.begin() + row, FilenameListItem(document));
0121     endInsertRows();
0122 
0123     // update all other items, since the common prefix path may have changed
0124     updateItems();
0125 
0126     return true;
0127 }
0128 
0129 bool detail::TabswitcherFilesModel::removeDocument(DocOrWidget document)
0130 {
0131     auto it = std::find_if(data_.begin(), data_.end(), [document](FilenameListItem &item) {
0132         return item.document == document;
0133     });
0134     if (it == data_.end()) {
0135         return false;
0136     }
0137 
0138     const int row = std::distance(data_.begin(), it);
0139     removeRow(row);
0140 
0141     return true;
0142 }
0143 
0144 bool detail::TabswitcherFilesModel::removeRows(int row, int count, const QModelIndex &parent)
0145 {
0146     Q_UNUSED(parent);
0147 
0148     if (row < 0 || row + count > rowCount()) {
0149         return false;
0150     }
0151 
0152     beginRemoveRows(QModelIndex(), row, row + count - 1);
0153     data_.erase(data_.begin() + row, data_.begin() + row + count);
0154     endRemoveRows();
0155 
0156     // update all other items, since the common prefix path may have changed
0157     updateItems();
0158 
0159     return true;
0160 }
0161 
0162 void detail::TabswitcherFilesModel::clear()
0163 {
0164     if (!data_.empty()) {
0165         beginResetModel();
0166         data_.clear();
0167         endResetModel();
0168     }
0169 }
0170 
0171 void detail::TabswitcherFilesModel::raiseDocument(DocOrWidget document)
0172 {
0173     // skip row 0, since row 0 is already correct
0174     for (int row = 1; row < rowCount(); ++row) {
0175         if (data_[row].document == document) {
0176             beginMoveRows(QModelIndex(), row, row, QModelIndex(), 0);
0177             std::rotate(data_.begin(), data_.begin() + row, data_.begin() + row + 1);
0178             endMoveRows();
0179             break;
0180         }
0181     }
0182 }
0183 
0184 DocOrWidget detail::TabswitcherFilesModel::item(int row) const
0185 {
0186     return data_[row].document;
0187 }
0188 
0189 void detail::TabswitcherFilesModel::updateItems()
0190 {
0191     post_process(data_);
0192     Q_EMIT dataChanged(createIndex(0, 0), createIndex(data_.size() - 1, 1), {});
0193 }
0194 
0195 int detail::TabswitcherFilesModel::columnCount(const QModelIndex &parent) const
0196 {
0197     Q_UNUSED(parent);
0198     return 2;
0199 }
0200 
0201 int detail::TabswitcherFilesModel::rowCount(const QModelIndex &parent) const
0202 {
0203     Q_UNUSED(parent);
0204     return data_.size();
0205 }
0206 
0207 QVariant detail::TabswitcherFilesModel::data(const QModelIndex &index, int role) const
0208 {
0209     if (role == Qt::DisplayRole) {
0210         const auto &row = data_[index.row()];
0211         if (index.column() == 0) {
0212             return row.displayPathPrefix;
0213         } else {
0214             return row.documentName();
0215         }
0216     } else if (role == Qt::DecorationRole) {
0217         if (index.column() == 1) {
0218             const auto &row = data_[index.row()];
0219             return row.icon();
0220         }
0221     } else if (role == Qt::ToolTipRole) {
0222         const auto &row = data_[index.row()];
0223         return row.fullPath();
0224     } else if (role == Qt::TextAlignmentRole) {
0225         if (index.column() == 0) {
0226             return QVariant(Qt::AlignRight | Qt::AlignVCenter);
0227         } else {
0228             return Qt::AlignVCenter;
0229         }
0230     } else if (role == Qt::ForegroundRole) {
0231         if (index.column() == 0) {
0232             return QBrush(Qt::darkGray);
0233         } else {
0234             return QVariant();
0235         }
0236     }
0237     return QVariant();
0238 }
0239 
0240 #include "moc_tabswitcherfilesmodel.cpp"