]> cloud.milkyroute.net Git - dolphin.git/blob - src/views/versioncontrol/versioncontrolobserver.cpp
Port the VCS plugin system to KPluginLoader
[dolphin.git] / src / views / versioncontrol / versioncontrolobserver.cpp
1 /*
2 * SPDX-FileCopyrightText: 2009 Peter Penz <peter.penz19@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "versioncontrolobserver.h"
8
9 #include "dolphin_versioncontrolsettings.h"
10 #include "dolphindebug.h"
11 #include "views/dolphinview.h"
12 #include "kitemviews/kfileitemmodel.h"
13 #include "updateitemstatesthread.h"
14
15 #include <KLocalizedString>
16 #include <KService>
17 #include <KServiceTypeTrader>
18 #include <KPluginLoader>
19 #include <KPluginMetaData>
20
21 #include <QTimer>
22
23 VersionControlObserver::VersionControlObserver(QObject* parent) :
24 QObject(parent),
25 m_pendingItemStatesUpdate(false),
26 m_silentUpdate(false),
27 m_view(nullptr),
28 m_model(nullptr),
29 m_dirVerificationTimer(nullptr),
30 m_pluginsInitialized(false),
31 m_plugin(nullptr),
32 m_updateItemStatesThread(nullptr)
33 {
34 // The verification timer specifies the timeout until the shown directory
35 // is checked whether it is versioned. Per default it is assumed that users
36 // don't iterate through versioned directories and a high timeout is used
37 // The timeout will be decreased as soon as a versioned directory has been
38 // found (see verifyDirectory()).
39 m_dirVerificationTimer = new QTimer(this);
40 m_dirVerificationTimer->setSingleShot(true);
41 m_dirVerificationTimer->setInterval(500);
42 connect(m_dirVerificationTimer, &QTimer::timeout,
43 this, &VersionControlObserver::verifyDirectory);
44 }
45
46 VersionControlObserver::~VersionControlObserver()
47 {
48 if (m_plugin) {
49 m_plugin->disconnect(this);
50 m_plugin = nullptr;
51 }
52 }
53
54 void VersionControlObserver::setModel(KFileItemModel* model)
55 {
56 if (m_model) {
57 disconnect(m_model, &KFileItemModel::itemsInserted,
58 this, &VersionControlObserver::delayedDirectoryVerification);
59 disconnect(m_model, &KFileItemModel::itemsChanged,
60 this, &VersionControlObserver::slotItemsChanged);
61 }
62
63 m_model = model;
64
65 if (model) {
66 connect(m_model, &KFileItemModel::itemsInserted,
67 this, &VersionControlObserver::delayedDirectoryVerification);
68 connect(m_model, &KFileItemModel::itemsChanged,
69 this, &VersionControlObserver::slotItemsChanged);
70 }
71 }
72
73 KFileItemModel* VersionControlObserver::model() const
74 {
75 return m_model;
76 }
77
78 void VersionControlObserver::setView(DolphinView* view)
79 {
80 if (m_view) {
81 disconnect(m_view, &DolphinView::activated,
82 this, &VersionControlObserver::delayedDirectoryVerification);
83 }
84
85 m_view = view;
86
87 if (m_view) {
88 connect(m_view, &DolphinView::activated,
89 this, &VersionControlObserver::delayedDirectoryVerification);
90 }
91 }
92
93 DolphinView* VersionControlObserver::view() const
94 {
95 return m_view;
96 }
97
98 QList<QAction*> VersionControlObserver::actions(const KFileItemList& items) const
99 {
100 bool hasNullItems = false;
101 for (const KFileItem& item : items) {
102 if (item.isNull()) {
103 qCWarning(DolphinDebug) << "Requesting version-control-actions for empty items";
104 hasNullItems = true;
105 break;
106 }
107 }
108
109 if (!m_model || hasNullItems) {
110 return {};
111 }
112
113 if (isVersionControlled()) {
114 return m_plugin->versionControlActions(items);
115 } else {
116 QList<QAction*> actions;
117 for (const QPointer<KVersionControlPlugin> &plugin : qAsConst(m_plugins)) {
118 actions << plugin->outOfVersionControlActions(items);
119 }
120 return actions;
121 }
122 }
123
124 void VersionControlObserver::delayedDirectoryVerification()
125 {
126 m_silentUpdate = false;
127 m_dirVerificationTimer->start();
128 }
129
130 void VersionControlObserver::silentDirectoryVerification()
131 {
132 m_silentUpdate = true;
133 m_dirVerificationTimer->start();
134 }
135
136 void VersionControlObserver::slotItemsChanged(const KItemRangeList& itemRanges, const QSet<QByteArray>& roles)
137 {
138 Q_UNUSED(itemRanges)
139
140 // Because "version" role is emitted by VCS plugin (ourselfs) we don't need to
141 // analyze it and update directory item states information. So lets check if
142 // there is only "version".
143 if ( !(roles.count() == 1 && roles.contains("version")) ) {
144 delayedDirectoryVerification();
145 }
146 }
147
148 void VersionControlObserver::verifyDirectory()
149 {
150 if (!m_model) {
151 return;
152 }
153
154 const KFileItem rootItem = m_model->rootItem();
155 if (rootItem.isNull() || !rootItem.url().isLocalFile()) {
156 return;
157 }
158
159 if (m_plugin != nullptr) {
160 if (!rootItem.url().path().startsWith(m_localRepoRoot) || !QFile::exists(m_localRepoRoot + '/' + m_plugin->fileName())) {
161 m_plugin = nullptr;
162
163 // The directory is not versioned. Reset the verification timer to a higher
164 // value, so that browsing through non-versioned directories is not slown down
165 // by an immediate verification.
166 m_dirVerificationTimer->setInterval(500);
167 } else {
168 // View was versionned but should not be anymore
169 updateItemStates();
170 }
171 } else if ((m_plugin = searchPlugin(rootItem.url()))) {
172 // The directory is versioned. Assume that the user will further browse through
173 // versioned directories and decrease the verification timer.
174 m_dirVerificationTimer->setInterval(100);
175 updateItemStates();
176 }
177 }
178
179 void VersionControlObserver::slotThreadFinished()
180 {
181 UpdateItemStatesThread* thread = m_updateItemStatesThread;
182 m_updateItemStatesThread = nullptr; // The thread deletes itself automatically (see updateItemStates())
183
184 if (!m_plugin || !thread) {
185 return;
186 }
187
188 const QMap<QString, QVector<ItemState> >& itemStates = thread->itemStates();
189 QMap<QString, QVector<ItemState> >::const_iterator it = itemStates.constBegin();
190 for (; it != itemStates.constEnd(); ++it) {
191 const QVector<ItemState>& items = it.value();
192
193 for (const ItemState& item : items) {
194 const KFileItem& fileItem = item.first;
195 const KVersionControlPlugin::ItemVersion version = item.second;
196 QHash<QByteArray, QVariant> values;
197 values.insert("version", QVariant(version));
198 m_model->setData(m_model->index(fileItem), values);
199 }
200 }
201
202 if (!m_silentUpdate) {
203 // Using an empty message results in clearing the previously shown information message and showing
204 // the default status bar information. This is useful as the user already gets feedback that the
205 // operation has been completed because of the icon emblems.
206 Q_EMIT operationCompletedMessage(QString());
207 }
208
209 if (m_pendingItemStatesUpdate) {
210 m_pendingItemStatesUpdate = false;
211 updateItemStates();
212 }
213 }
214
215 void VersionControlObserver::updateItemStates()
216 {
217 Q_ASSERT(m_plugin);
218 if (m_updateItemStatesThread) {
219 // An update is currently ongoing. Wait until the thread has finished
220 // the update (see slotThreadFinished()).
221 m_pendingItemStatesUpdate = true;
222 return;
223 }
224
225 QMap<QString, QVector<ItemState> > itemStates;
226 createItemStatesList(itemStates);
227
228 if (!itemStates.isEmpty()) {
229 if (!m_silentUpdate) {
230 Q_EMIT infoMessage(i18nc("@info:status", "Updating version information..."));
231 }
232 m_updateItemStatesThread = new UpdateItemStatesThread(m_plugin, itemStates);
233 connect(m_updateItemStatesThread, &UpdateItemStatesThread::finished,
234 this, &VersionControlObserver::slotThreadFinished);
235 connect(m_updateItemStatesThread, &UpdateItemStatesThread::finished,
236 m_updateItemStatesThread, &UpdateItemStatesThread::deleteLater);
237
238 m_updateItemStatesThread->start(); // slotThreadFinished() is called when finished
239 }
240 }
241
242 int VersionControlObserver::createItemStatesList(QMap<QString, QVector<ItemState> >& itemStates,
243 const int firstIndex)
244 {
245 const int itemCount = m_model->count();
246 const int currentExpansionLevel = m_model->expandedParentsCount(firstIndex);
247
248 QVector<ItemState> items;
249 items.reserve(itemCount - firstIndex);
250
251 int index;
252 for (index = firstIndex; index < itemCount; ++index) {
253 const int expansionLevel = m_model->expandedParentsCount(index);
254
255 if (expansionLevel == currentExpansionLevel) {
256 ItemState itemState;
257 itemState.first = m_model->fileItem(index);
258 itemState.second = KVersionControlPlugin::UnversionedVersion;
259
260 items.append(itemState);
261 } else if (expansionLevel > currentExpansionLevel) {
262 // Sub folder
263 index += createItemStatesList(itemStates, index) - 1;
264 } else {
265 break;
266 }
267 }
268
269 if (!items.isEmpty()) {
270 const QUrl& url = items.first().first.url();
271 itemStates.insert(url.adjusted(QUrl::RemoveFilename).path(), items);
272 }
273
274 return index - firstIndex; // number of processed items
275 }
276
277 void VersionControlObserver::initPlugins()
278 {
279 if (!m_pluginsInitialized) {
280 // No searching for plugins has been done yet. Query the KServiceTypeTrader for
281 // all fileview version control plugins and remember them in 'plugins'.
282 const QStringList enabledPlugins = VersionControlSettings::enabledPlugins();
283
284 const QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral("dolphin/vcs"));
285
286 QSet<QString> loadedPlugins;
287
288 for (const auto &p : plugins) {
289 if (enabledPlugins.contains(p.name())) {
290 KPluginLoader loader(p.fileName());
291 KPluginFactory *factory = loader.factory();
292 KVersionControlPlugin *plugin = factory->create<KVersionControlPlugin>();
293 if (plugin) {
294 m_plugins.append(plugin);
295 loadedPlugins += p.name();
296 }
297 }
298 }
299
300 // Deprecated: load plugins using KService. This mechanism will be removed with KF6
301 const KService::List pluginServices = KServiceTypeTrader::self()->query(QStringLiteral("FileViewVersionControlPlugin"));
302 for (KService::List::ConstIterator it = pluginServices.constBegin(); it != pluginServices.constEnd(); ++it) {
303 if (loadedPlugins.contains((*it)->property("Name", QVariant::String).toString())) {
304 continue;
305 }
306 if (enabledPlugins.contains((*it)->name())) {
307 KVersionControlPlugin* plugin = (*it)->createInstance<KVersionControlPlugin>(this);
308 if (plugin) {
309 m_plugins.append(plugin);
310 }
311 }
312 }
313
314 for (auto &plugin : qAsConst(m_plugins)) {
315 connect(plugin, &KVersionControlPlugin::itemVersionsChanged,
316 this, &VersionControlObserver::silentDirectoryVerification);
317 connect(plugin, &KVersionControlPlugin::infoMessage,
318 this, &VersionControlObserver::infoMessage);
319 connect(plugin, &KVersionControlPlugin::errorMessage,
320 this, &VersionControlObserver::errorMessage);
321 connect(plugin, &KVersionControlPlugin::operationCompletedMessage,
322 this, &VersionControlObserver::operationCompletedMessage);
323 }
324
325 m_pluginsInitialized = true;
326 }
327 }
328
329 KVersionControlPlugin* VersionControlObserver::searchPlugin(const QUrl& directory)
330 {
331 initPlugins();
332
333 // Verify whether the current directory is under a version system
334 for (const QPointer<KVersionControlPlugin> &plugin : qAsConst(m_plugins)) {
335 if (!plugin) {
336 continue;
337 }
338
339 // first naively check if we are at working copy root
340 const QString fileName = directory.path() + '/' + plugin->fileName();
341 if (QFile::exists(fileName)) {
342 m_localRepoRoot = directory.path();
343 return plugin;
344 }
345 const QString root = plugin->localRepositoryRoot(directory.path());
346 if (!root.isEmpty()) {
347 m_localRepoRoot = root;
348 return plugin;
349 }
350 }
351 return nullptr;
352 }
353
354 bool VersionControlObserver::isVersionControlled() const
355 {
356 return m_plugin != nullptr;
357 }
358