File indexing completed on 2024-05-12 05:47:52
0001 /* 0002 * SPDX-FileCopyrightText: 2009 Peter Penz <peter.penz19@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "versioncontrolobserver.h" 0008 0009 #include "dolphin_versioncontrolsettings.h" 0010 #include "dolphindebug.h" 0011 #include "kitemviews/kfileitemmodel.h" 0012 #include "updateitemstatesthread.h" 0013 #include "views/dolphinview.h" 0014 0015 #include <KLocalizedString> 0016 #include <KPluginFactory> 0017 #include <KPluginMetaData> 0018 0019 #include <QTimer> 0020 0021 VersionControlObserver::VersionControlObserver(QObject *parent) 0022 : QObject(parent) 0023 , m_pendingItemStatesUpdate(false) 0024 , m_silentUpdate(false) 0025 , m_view(nullptr) 0026 , m_model(nullptr) 0027 , m_dirVerificationTimer(nullptr) 0028 , m_pluginsInitialized(false) 0029 , m_plugin(nullptr) 0030 , m_updateItemStatesThread(nullptr) 0031 { 0032 // The verification timer specifies the timeout until the shown directory 0033 // is checked whether it is versioned. Per default it is assumed that users 0034 // don't iterate through versioned directories and a high timeout is used 0035 // The timeout will be decreased as soon as a versioned directory has been 0036 // found (see verifyDirectory()). 0037 m_dirVerificationTimer = new QTimer(this); 0038 m_dirVerificationTimer->setSingleShot(true); 0039 m_dirVerificationTimer->setInterval(500); 0040 connect(m_dirVerificationTimer, &QTimer::timeout, this, &VersionControlObserver::verifyDirectory); 0041 } 0042 0043 VersionControlObserver::~VersionControlObserver() 0044 { 0045 if (m_plugin) { 0046 m_plugin->disconnect(this); 0047 m_plugin = nullptr; 0048 } 0049 } 0050 0051 void VersionControlObserver::setModel(KFileItemModel *model) 0052 { 0053 if (m_model) { 0054 disconnect(m_model, &KFileItemModel::itemsInserted, this, &VersionControlObserver::delayedDirectoryVerification); 0055 disconnect(m_model, &KFileItemModel::itemsChanged, this, &VersionControlObserver::slotItemsChanged); 0056 } 0057 0058 m_model = model; 0059 0060 if (model) { 0061 connect(m_model, &KFileItemModel::itemsInserted, this, &VersionControlObserver::delayedDirectoryVerification); 0062 connect(m_model, &KFileItemModel::itemsChanged, this, &VersionControlObserver::slotItemsChanged); 0063 } 0064 } 0065 0066 KFileItemModel *VersionControlObserver::model() const 0067 { 0068 return m_model; 0069 } 0070 0071 void VersionControlObserver::setView(DolphinView *view) 0072 { 0073 if (m_view) { 0074 disconnect(m_view, &DolphinView::activated, this, &VersionControlObserver::delayedDirectoryVerification); 0075 } 0076 0077 m_view = view; 0078 0079 if (m_view) { 0080 connect(m_view, &DolphinView::activated, this, &VersionControlObserver::delayedDirectoryVerification); 0081 } 0082 } 0083 0084 DolphinView *VersionControlObserver::view() const 0085 { 0086 return m_view; 0087 } 0088 0089 QList<QAction *> VersionControlObserver::actions(const KFileItemList &items) const 0090 { 0091 bool hasNullItems = false; 0092 for (const KFileItem &item : items) { 0093 if (item.isNull()) { 0094 qCWarning(DolphinDebug) << "Requesting version-control-actions for empty items"; 0095 hasNullItems = true; 0096 break; 0097 } 0098 } 0099 0100 if (!m_model || hasNullItems) { 0101 return {}; 0102 } 0103 0104 if (isVersionControlled()) { 0105 return m_plugin->versionControlActions(items); 0106 } else { 0107 QList<QAction *> actions; 0108 for (const QPointer<KVersionControlPlugin> &plugin : std::as_const(m_plugins)) { 0109 actions << plugin->outOfVersionControlActions(items); 0110 } 0111 return actions; 0112 } 0113 } 0114 0115 void VersionControlObserver::delayedDirectoryVerification() 0116 { 0117 m_silentUpdate = false; 0118 m_dirVerificationTimer->start(); 0119 } 0120 0121 void VersionControlObserver::silentDirectoryVerification() 0122 { 0123 m_silentUpdate = true; 0124 m_dirVerificationTimer->start(); 0125 } 0126 0127 void VersionControlObserver::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles) 0128 { 0129 Q_UNUSED(itemRanges) 0130 0131 // Because "version" role is emitted by VCS plugin (ourselves) we don't need to 0132 // analyze it and update directory item states information. So lets check if 0133 // there is only "version". 0134 if (!(roles.count() == 1 && roles.contains("version"))) { 0135 delayedDirectoryVerification(); 0136 } 0137 } 0138 0139 void VersionControlObserver::verifyDirectory() 0140 { 0141 if (!m_model) { 0142 return; 0143 } 0144 0145 const KFileItem rootItem = m_model->rootItem(); 0146 if (rootItem.isNull() || !rootItem.url().isLocalFile()) { 0147 return; 0148 } 0149 0150 if (m_plugin != nullptr) { 0151 if (!rootItem.url().path().startsWith(m_localRepoRoot) || !QFile::exists(m_localRepoRoot + '/' + m_plugin->fileName())) { 0152 m_plugin = nullptr; 0153 0154 // The directory is not versioned. Reset the verification timer to a higher 0155 // value, so that browsing through non-versioned directories is not slown down 0156 // by an immediate verification. 0157 m_dirVerificationTimer->setInterval(500); 0158 } else { 0159 // View was versioned but should not be anymore 0160 updateItemStates(); 0161 } 0162 } else if ((m_plugin = searchPlugin(rootItem.url()))) { 0163 // The directory is versioned. Assume that the user will further browse through 0164 // versioned directories and decrease the verification timer. 0165 m_dirVerificationTimer->setInterval(100); 0166 updateItemStates(); 0167 } 0168 } 0169 0170 void VersionControlObserver::slotThreadFinished() 0171 { 0172 UpdateItemStatesThread *thread = m_updateItemStatesThread; 0173 m_updateItemStatesThread = nullptr; // The thread deletes itself automatically (see updateItemStates()) 0174 0175 if (!m_plugin || !thread) { 0176 return; 0177 } 0178 0179 const QMap<QString, QVector<ItemState>> &itemStates = thread->itemStates(); 0180 QMap<QString, QVector<ItemState>>::const_iterator it = itemStates.constBegin(); 0181 for (; it != itemStates.constEnd(); ++it) { 0182 const QVector<ItemState> &items = it.value(); 0183 0184 for (const ItemState &item : items) { 0185 const KFileItem &fileItem = item.first; 0186 const KVersionControlPlugin::ItemVersion version = item.second; 0187 QHash<QByteArray, QVariant> values; 0188 values.insert("version", QVariant(version)); 0189 m_model->setData(m_model->index(fileItem), values); 0190 } 0191 } 0192 0193 if (!m_silentUpdate) { 0194 // Using an empty message results in clearing the previously shown information message and showing 0195 // the default status bar information. This is useful as the user already gets feedback that the 0196 // operation has been completed because of the icon emblems. 0197 Q_EMIT operationCompletedMessage(QString()); 0198 } 0199 0200 if (m_pendingItemStatesUpdate) { 0201 m_pendingItemStatesUpdate = false; 0202 updateItemStates(); 0203 } 0204 } 0205 0206 void VersionControlObserver::updateItemStates() 0207 { 0208 Q_ASSERT(m_plugin); 0209 if (m_updateItemStatesThread) { 0210 // An update is currently ongoing. Wait until the thread has finished 0211 // the update (see slotThreadFinished()). 0212 m_pendingItemStatesUpdate = true; 0213 return; 0214 } 0215 0216 QMap<QString, QVector<ItemState>> itemStates; 0217 createItemStatesList(itemStates); 0218 0219 if (!itemStates.isEmpty()) { 0220 if (!m_silentUpdate) { 0221 Q_EMIT infoMessage(i18nc("@info:status", "Updating version information…")); 0222 } 0223 m_updateItemStatesThread = new UpdateItemStatesThread(m_plugin, itemStates); 0224 connect(m_updateItemStatesThread, &UpdateItemStatesThread::finished, this, &VersionControlObserver::slotThreadFinished); 0225 connect(m_updateItemStatesThread, &UpdateItemStatesThread::finished, m_updateItemStatesThread, &UpdateItemStatesThread::deleteLater); 0226 0227 m_updateItemStatesThread->start(); // slotThreadFinished() is called when finished 0228 } 0229 } 0230 0231 int VersionControlObserver::createItemStatesList(QMap<QString, QVector<ItemState>> &itemStates, const int firstIndex) 0232 { 0233 const int itemCount = m_model->count(); 0234 const int currentExpansionLevel = m_model->expandedParentsCount(firstIndex); 0235 0236 QVector<ItemState> items; 0237 items.reserve(itemCount - firstIndex); 0238 0239 int index; 0240 for (index = firstIndex; index < itemCount; ++index) { 0241 const int expansionLevel = m_model->expandedParentsCount(index); 0242 0243 if (expansionLevel == currentExpansionLevel) { 0244 ItemState itemState; 0245 itemState.first = m_model->fileItem(index); 0246 itemState.second = KVersionControlPlugin::UnversionedVersion; 0247 0248 items.append(itemState); 0249 } else if (expansionLevel > currentExpansionLevel) { 0250 // Sub folder 0251 index += createItemStatesList(itemStates, index) - 1; 0252 } else { 0253 break; 0254 } 0255 } 0256 0257 if (!items.isEmpty()) { 0258 const QUrl &url = items.first().first.url(); 0259 itemStates.insert(url.adjusted(QUrl::RemoveFilename).path(), items); 0260 } 0261 0262 return index - firstIndex; // number of processed items 0263 } 0264 0265 void VersionControlObserver::initPlugins() 0266 { 0267 if (!m_pluginsInitialized) { 0268 // No searching for plugins has been done yet. Query all fileview version control 0269 // plugins and remember them in 'plugins'. 0270 const QStringList enabledPlugins = VersionControlSettings::enabledPlugins(); 0271 0272 const QVector<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("dolphin/vcs")); 0273 0274 QSet<QString> loadedPlugins; 0275 0276 for (const auto &p : plugins) { 0277 if (enabledPlugins.contains(p.name())) { 0278 auto plugin = KPluginFactory::instantiatePlugin<KVersionControlPlugin>(p, parent()).plugin; 0279 if (plugin) { 0280 m_plugins.append(plugin); 0281 loadedPlugins += p.name(); 0282 } 0283 } 0284 } 0285 0286 for (auto &plugin : std::as_const(m_plugins)) { 0287 connect(plugin, &KVersionControlPlugin::itemVersionsChanged, this, &VersionControlObserver::silentDirectoryVerification); 0288 connect(plugin, &KVersionControlPlugin::infoMessage, this, &VersionControlObserver::infoMessage); 0289 connect(plugin, &KVersionControlPlugin::errorMessage, this, &VersionControlObserver::errorMessage); 0290 connect(plugin, &KVersionControlPlugin::operationCompletedMessage, this, &VersionControlObserver::operationCompletedMessage); 0291 } 0292 0293 m_pluginsInitialized = true; 0294 } 0295 } 0296 0297 KVersionControlPlugin *VersionControlObserver::searchPlugin(const QUrl &directory) 0298 { 0299 initPlugins(); 0300 0301 // Verify whether the current directory is under a version system 0302 for (const QPointer<KVersionControlPlugin> &plugin : std::as_const(m_plugins)) { 0303 if (!plugin) { 0304 continue; 0305 } 0306 0307 // first naively check if we are at working copy root 0308 const QString fileName = directory.path() + '/' + plugin->fileName(); 0309 if (QFile::exists(fileName)) { 0310 m_localRepoRoot = directory.path(); 0311 return plugin; 0312 } 0313 const QString root = plugin->localRepositoryRoot(directory.path()); 0314 if (!root.isEmpty()) { 0315 m_localRepoRoot = root; 0316 return plugin; 0317 } 0318 } 0319 return nullptr; 0320 } 0321 0322 bool VersionControlObserver::isVersionControlled() const 0323 { 0324 return m_plugin != nullptr; 0325 } 0326 0327 #include "moc_versioncontrolobserver.cpp"