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"