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