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"