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