1 /*
2 * SPDX-FileCopyrightText: 2012-2015 Andreas Cord-Landwehr <cordlandwehr@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6
7 #include "project.h"
8 #include "libgraphtheory/graphdocument.h"
9 #include <KConfig>
10 #include <KConfigGroup>
11 #include <KLocalizedString>
12 #include <KMessageBox>
13 #include <KTextEditor/Document>
14 #include <KTextEditor/Editor>
15 #include <KTar>
16 #include <QUrl>
17 #include <QDir>
18 #include <QHash>
19 #include <QMap>
20 #include <QSaveFile>
21 #include <QTemporaryFile>
22 #include <QTemporaryDir>
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QJsonArray>
26 #include <QDebug>
27
28 using namespace GraphTheory;
29
30 class ProjectPrivate
31 {
32 public:
ProjectPrivate()33 ProjectPrivate()
34 : m_journal(nullptr)
35 , m_graphEditor(nullptr)
36 , m_modified(false)
37 , m_activeGraphDocumentIndex(-1)
38 , m_activeCodeDocumentIndex(-1)
39 {
40
41 }
42
43 QUrl m_projectUrl; //!< the project's archive file
44 QTemporaryDir m_workingDirectory; //!< temporary directory where all project files are organized
45 QList<KTextEditor::Document*> m_codeDocuments;
46 QList<GraphDocumentPtr> m_graphDocuments;
47 QHash<KTextEditor::Document*,QString> m_documentNames;
48 KTextEditor::Document *m_journal;
49 GraphTheory::Editor *m_graphEditor;
50 bool m_modified;
51
52 int m_activeGraphDocumentIndex;
53 int m_activeCodeDocumentIndex;
54
55 /**
56 * Set project from project archive file.
57 */
58 bool loadProject(const QUrl &path);
59
60 /**
61 * Write project meta info file
62 */
63 bool writeProjectMetaInfo();
64 };
65
loadProject(const QUrl & url)66 bool ProjectPrivate::loadProject(const QUrl &url)
67 {
68 // extract archive file to temporary working directory
69 KTar tar = KTar(url.toLocalFile(), QStringLiteral("application/gzip"));
70 if (!tar.open(QIODevice::ReadOnly)) {
71 qCritical() << "Could not open project archive file for reading, aborting.";
72 return false;
73 }
74 tar.directory()->copyTo(m_workingDirectory.path(), true);
75 QFile metaInfoFile(m_workingDirectory.path() + QChar('/') + "project.json");
76 if (!metaInfoFile.open(QIODevice::ReadOnly)) {
77 qWarning("Could not open project.json file for reading, aborting.");
78 return false;
79 }
80 QJsonDocument metaInfoDoc = QJsonDocument::fromJson(metaInfoFile.readAll());
81
82 // set project
83 QJsonObject metaInfo = metaInfoDoc.object();
84
85 QJsonArray codeDocs = metaInfo["scripts"].toArray();
86 QJsonArray codeDocNames = metaInfo["scriptNames"].toArray();
87 for (int index = 0; index < codeDocs.count(); ++index) {
88 QJsonObject docInfo = codeDocs.at(index).toObject();
89 QString filename = docInfo["file"].toString();
90 KTextEditor::Document *document = KTextEditor::Editor::instance()->createDocument(nullptr);
91 document->openUrl(QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + filename));
92 m_codeDocuments.append(document);
93 m_documentNames.insert(document, codeDocNames.at(index).toString());
94 }
95
96 QJsonArray graphDocs = metaInfo["graphs"].toArray();
97 for (int index = 0; index < graphDocs.count(); ++index) {
98 QJsonObject docInfo = graphDocs.at(index).toObject();
99 QString fileName = docInfo["file"].toString();
100 QUrl fileUrl = QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + fileName);
101 GraphDocumentPtr document = m_graphEditor->openDocument(fileUrl);
102 // add only to project, if document was loaded correctly
103 if (document) {
104 document->setDocumentName(docInfo["name"].toString());
105 m_graphDocuments.append(document);
106 }
107 }
108 m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
109 m_journal->openUrl(QUrl::fromLocalFile(m_workingDirectory.path() + QChar('/') + metaInfo["journal.txt"].toString()));
110 Q_ASSERT(m_journal != nullptr);
111
112 //TODO save & load open document index
113
114 return true;
115 }
116
writeProjectMetaInfo()117 bool ProjectPrivate::writeProjectMetaInfo()
118 {
119 QJsonObject metaInfo;
120
121 QJsonArray codeDocs, codeDocNames, graphDocs;
122 for (KTextEditor::Document *document : std::as_const(m_codeDocuments)) {
123 QJsonObject docInfo;
124 docInfo.insert("file", document->url().fileName());
125 codeDocs.append(docInfo);
126 codeDocNames.append(m_documentNames.value(document));
127 }
128 for (const GraphTheory::GraphDocumentPtr &document : std::as_const(m_graphDocuments)) {
129 QJsonObject docInfo;
130 docInfo.insert("file", document->documentUrl().fileName());
131 docInfo.insert("name", document->documentName());
132 graphDocs.append(docInfo);
133 }
134 metaInfo.insert("scripts", codeDocs);
135 metaInfo.insert("scriptNames", codeDocNames);
136 metaInfo.insert("graphs", graphDocs);
137 metaInfo.insert("journal", m_journal->url().fileName());
138
139 // write to file
140 QFile metaInfoFile(m_workingDirectory.path() + QChar('/') + "project.json");
141 if (!metaInfoFile.open(QIODevice::WriteOnly)) {
142 qWarning("Couldn't open project.json file for writing, abort.");
143 return false;
144 }
145 QJsonDocument metaInfoDoc(metaInfo);
146 metaInfoFile.write(metaInfoDoc.toJson());
147
148 return true;
149 }
150
151
152 //TODO make graphEditor singleton
Project(GraphTheory::Editor * graphEditor)153 Project::Project(GraphTheory::Editor *graphEditor)
154 : d(new ProjectPrivate)
155 {
156 d->m_graphEditor = graphEditor;
157 d->m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
158 d->m_journal->saveAs(QUrl::fromLocalFile(workingDir() + QChar('/') + QString("journal.txt")));
159 }
160
161 //TODO make graphEditor singleton
Project(const QUrl & projectFile,GraphTheory::Editor * graphEditor)162 Project::Project(const QUrl &projectFile, GraphTheory::Editor *graphEditor)
163 : d(new ProjectPrivate)
164 {
165 d->m_graphEditor = graphEditor;
166 d->m_projectUrl = projectFile;
167 if (!d->loadProject(projectFile)) {
168 addGraphDocument(graphEditor->createDocument());
169 setModified(false);
170 d->m_journal = KTextEditor::Editor::instance()->createDocument(nullptr);
171
172 KMessageBox::error(nullptr,
173 i18nc("@info",
174 "The Rocs project could not be imported because the project file could not be parsed."));
175 }
176
177 for (const auto &document : d->m_codeDocuments) {
178 connect(document, &KTextEditor::Document::modifiedChanged,
179 this, &Project::modifiedChanged);
180 }
181 for (const auto &document : d->m_graphDocuments) {
182 connect(document.data(), &GraphDocument::modifiedChanged,
183 this, &Project::modifiedChanged);
184 }
185 }
186
~Project()187 Project::~Project()
188 {
189 }
190
projectUrl() const191 QUrl Project::projectUrl() const
192 {
193 return d->m_projectUrl;
194 }
195
setProjectUrl(const QUrl & url)196 void Project::setProjectUrl(const QUrl &url)
197 {
198 d->m_projectUrl = url;
199 }
200
workingDir() const201 QString Project::workingDir() const
202 {
203 return d->m_workingDirectory.path();
204 }
205
createCodeDocument(const QString & filePath)206 KTextEditor::Document* Project::createCodeDocument(const QString& filePath)
207 {
208 auto path = d->m_workingDirectory.path() + QLatin1Char('/') + filePath + QStringLiteral(".js");
209
210 auto doc = KTextEditor::Editor::instance()->createDocument(nullptr);
211 if (!doc->saveAs(QUrl::fromLocalFile(path))) {
212 qCritical() << "Error when saving code file to working directory, aborting.";
213 return nullptr;
214 }
215
216 if (!addCodeDocument(doc)) {
217 return nullptr;
218 }
219
220 return doc;
221 }
222
openCodeDocument(const QUrl & url)223 KTextEditor::Document* Project::openCodeDocument(const QUrl &url)
224 {
225 auto doc = KTextEditor::Editor::instance()->createDocument(nullptr);
226 if (!doc->openUrl(url)) {
227 qCritical() << "Error when opening code file, aborting.";
228 return nullptr;
229 }
230
231 if (!addCodeDocument(doc)) {
232 return nullptr;
233 }
234
235 return doc;
236 }
237
addCodeDocument(KTextEditor::Document * document)238 bool Project::addCodeDocument(KTextEditor::Document *document)
239 {
240 if (document == nullptr) {
241 return false;
242 }
243
244 Q_EMIT codeDocumentAboutToBeAdded(document, d->m_codeDocuments.count());
245 connect(document, &KTextEditor::Document::modifiedChanged,
246 this, &Project::modifiedChanged);
247 d->m_codeDocuments.append(document);
248 Q_EMIT codeDocumentAdded();
249 setModified(true);
250 return true;
251 }
252
importCodeDocument(const QUrl & url)253 KTextEditor::Document * Project::importCodeDocument(const QUrl &url)
254 {
255 return openCodeDocument(url);
256 }
257
tryToRemoveCodeDocument(KTextEditor::Document * document)258 void Project::tryToRemoveCodeDocument(KTextEditor::Document *document)
259 {
260 QString path = document->url().toString();
261 if(!document->closeUrl())
262 return;
263 int index = d->m_codeDocuments.indexOf(document);
264 Q_EMIT codeDocumentAboutToBeRemoved(index, index);
265 disconnect(document, &KTextEditor::Document::modifiedChanged,
266 this, &Project::modifiedChanged);
267 d->m_codeDocuments.removeAt(index);
268 Q_EMIT codeDocumentRemoved();
269 if (!path.startsWith(d->m_workingDirectory.path())) {
270 qCritical() << "Aborting removal of document: not in temporary working directory";
271 return;
272 }
273 if (!QFile::remove(path)) {
274 qCritical() << "Could not remove code file" << path;
275 }
276 d->m_documentNames.remove(document);
277 setModified(true);
278 }
279
documentName(KTextEditor::Document * document) const280 QString Project::documentName(KTextEditor::Document *document) const
281 {
282 const QString docName = d->m_documentNames.value(document);
283 return !docName.isEmpty()
284 ? docName
285 : document->documentName();
286 }
287
setDocumentName(KTextEditor::Document * document,const QString & name)288 void Project::setDocumentName(KTextEditor::Document *document, const QString &name)
289 {
290 d->m_documentNames.insert(document, name);
291 setModified(true);
292 }
293
codeDocuments() const294 QList<KTextEditor::Document*> Project::codeDocuments() const
295 {
296 return d->m_codeDocuments;
297 }
298
setActiveCodeDocument(int index)299 void Project::setActiveCodeDocument(int index)
300 {
301 if (index < 0 || index >= d->m_codeDocuments.count()) {
302 qCritical() << "Code document index invalid, aborting change of current document.";
303 return;
304 }
305 d->m_activeCodeDocumentIndex = index;
306 Q_EMIT activeCodeDocumentChanged(index);
307 }
308
graphDocuments() const309 QList<GraphDocumentPtr> Project::graphDocuments() const
310 {
311 return d->m_graphDocuments;
312 }
313
addGraphDocument(GraphDocumentPtr document)314 bool Project::addGraphDocument(GraphDocumentPtr document)
315 {
316 // compute first unused document path
317 QStringList usedFileNames;
318 for (const GraphTheory::GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
319 usedFileNames.append(document->documentUrl().fileName());
320 }
321 QString fileName;
322 for (int i = 0; i <= d->m_graphDocuments.count(); ++i) {
323 fileName = "graphfile" + QString::number(i) + QString(".graph2");
324 if (!usedFileNames.contains(fileName)) {
325 break;
326 }
327 }
328 QString path = d->m_workingDirectory.path()
329 + QChar('/')
330 + fileName;
331
332 // put document into working directory
333 document->documentSaveAs(QUrl::fromLocalFile(path));
334 int index = d->m_graphDocuments.length();
335 Q_EMIT graphDocumentAboutToBeAdded(document, index);
336 connect(document.data(), &GraphDocument::modifiedChanged,
337 this, &Project::modifiedChanged);
338 d->m_graphDocuments.append(document);
339 Q_EMIT graphDocumentAdded();
340 setModified(true);
341
342 if (d->m_activeGraphDocumentIndex < 0) {
343 setActiveGraphDocument(index);
344 }
345 return true;
346 }
347
importGraphDocument(const QUrl & documentUrl)348 GraphTheory::GraphDocumentPtr Project::importGraphDocument(const QUrl &documentUrl)
349 {
350 Q_ASSERT(d->m_graphEditor);
351 GraphTheory::GraphDocumentPtr document = d->m_graphEditor->openDocument(documentUrl);
352 Q_ASSERT(document);
353 addGraphDocument(document);
354 return document;
355 }
356
removeGraphDocument(GraphDocumentPtr document)357 void Project::removeGraphDocument(GraphDocumentPtr document)
358 {
359 QString path = document->documentUrl().toLocalFile();
360 d->m_graphDocuments.removeAll(document);
361 if (!path.startsWith(d->m_workingDirectory.path())) {
362 qCritical() << "Aborting removal of graph document with path "
363 << path
364 << ", not in temporary working directory" << d->m_workingDirectory.path();
365 return;
366 }
367 if (!QFile::remove(path)) {
368 qCritical() << "Could not remove graph file" << path;
369 }
370 int index = d->m_graphDocuments.indexOf(document);
371 Q_EMIT graphDocumentAboutToBeRemoved(index, index);
372 d->m_graphDocuments.removeAt(index);
373 Q_EMIT graphDocumentRemoved();
374 setModified(true);
375 }
376
setActiveGraphDocument(int index)377 void Project::setActiveGraphDocument(int index)
378 {
379 if (index < 0 || index >= d->m_graphDocuments.count()) {
380 qCritical() << "Graph document index invalid, aborting change of current document.";
381 return;
382 }
383 d->m_activeGraphDocumentIndex = index;
384 Q_EMIT activeGraphDocumentChanged(index);
385 Q_EMIT activeGraphDocumentChanged(d->m_graphDocuments.at(index));
386 }
387
activeGraphDocument() const388 GraphDocumentPtr Project::activeGraphDocument() const
389 {
390 if (d->m_activeGraphDocumentIndex < 0 || d->m_graphDocuments.count() <= d->m_activeGraphDocumentIndex) {
391 return GraphDocumentPtr();
392 }
393 return d->m_graphDocuments.at(d->m_activeGraphDocumentIndex);
394 }
395
journalDocument() const396 KTextEditor::Document * Project::journalDocument() const
397 {
398 return d->m_journal;
399 }
400
projectSave()401 bool Project::projectSave()
402 {
403 if (d->m_projectUrl.isEmpty()) {
404 qCritical() << "No project file specified, abort saving.";
405 return false;
406 }
407 KTar tar = KTar(d->m_projectUrl.toLocalFile(), QStringLiteral("application/gzip"));
408 tar.open(QIODevice::WriteOnly);
409
410 for (KTextEditor::Document *document : std::as_const(d->m_codeDocuments)) {
411 document->save();
412 tar.addLocalFile(document->url().toLocalFile(), document->url().fileName());
413 }
414 for (const GraphTheory::GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
415 document->documentSave();
416 tar.addLocalFile(document->documentUrl().toLocalFile(), document->documentUrl().fileName());
417 }
418 tar.addLocalFile(d->m_journal->url().toLocalFile(), "journal.txt");
419 d->writeProjectMetaInfo();
420 tar.addLocalFile(d->m_workingDirectory.path() + QChar('/') + "project.json", "project.json");
421 tar.close();
422
423 // update modified state
424 setModified(false);
425
426 return true;
427 }
428
projectSaveAs(const QUrl & url)429 bool Project::projectSaveAs(const QUrl &url)
430 {
431 d->m_projectUrl = url;
432 return projectSave();
433 }
434
setModified(bool modified)435 void Project::setModified(bool modified)
436 {
437 if (modified == d->m_modified) {
438 return;
439 }
440 d->m_modified = modified;
441 Q_EMIT modifiedChanged();
442 }
443
isModified() const444 bool Project::isModified() const
445 {
446 for (const GraphDocumentPtr &document : std::as_const(d->m_graphDocuments)) {
447 if (document->isModified()) {
448 return true;
449 }
450 }
451 for (KTextEditor::Document *document : std::as_const(d->m_codeDocuments)) {
452 if (document->isModified()) {
453 return true;
454 }
455 }
456 if (d->m_journal && d->m_journal->isModified()) {
457 return true;
458 }
459
460 return d->m_modified;
461 }
462