1/* This file is part of the Kate project.
2 *
3 * Copyright (C) 2012 Christoph Cullmann <cullmann@kde.org>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB. If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20
21#include "kateproject.h"
22#include "kateprojectworker.h"
23
24#include <klocale.h>
25
26#include <ktexteditor/document.h>
27
28#include <QDir>
29#include <QFile>
30#include <QFileInfo>
31#include <QPlainTextDocumentLayout>
32#include <qjson/parser.h>
33
34KateProject::KateProject ()
35 : QObject ()
36 , m_worker (new KateProjectWorker (this))
37 , m_thread (m_worker)
38 , m_notesDocument (0)
39 , m_documentsParent (0)
40{
41 /**
42 * move worker object over and start our worker thread
43 * thread will delete worker on run() exit
44 */
45 m_worker->moveToThread (&m_thread);
46 m_thread.start ();
47}
48
49KateProject::~KateProject ()
50{
51 /**
52 * only do this once
53 */
54 Q_ASSERT (m_worker);
55
56 /**
57 * quit the thread event loop and wait for completion
58 * will delete worker on thread run() exit
59 */
60 m_thread.quit ();
61 m_thread.wait ();
62
63 /**
64 * marks as deleted
65 */
66 m_worker = 0;
67
68 /**
69 * save notes document, if any
70 */
71 saveNotesDocument ();
72}
73
74bool KateProject::load (const QString &fileName)
75{
76 /**
77 * bail out if already fileName set!
78 */
79 if (!m_fileName.isEmpty())
80 return false;
81
82 /**
83 * set new filename and base directory
84 */
85 m_fileName = fileName;
86 m_baseDir = QFileInfo(m_fileName).canonicalPath();
87
88 /**
89 * trigger reload
90 */
91 return reload ();
92}
93
94bool KateProject::reload (bool force)
95{
96 /**
97 * open the file for reading, bail out on error!
98 */
99 QFile file (m_fileName);
100 if (!file.open (QFile::ReadOnly))
101 return false;
102
103 /**
104 * parse the whole file, bail out again on error!
105 */
106 bool ok = true;
107 QJson::Parser parser;
108 QVariant project = parser.parse (&file, &ok);
109 if (!ok)
110 return false;
111
112 /**
113 * now: get global group
114 */
115 QVariantMap globalProject = project.toMap ();
116
117 /**
118 * no name, bad => bail out
119 */
120 if (globalProject["name"].toString().isEmpty())
121 return false;
122
123 /**
124 * support out-of-source project files
125 */
126 if (!globalProject["directory"].toString().isEmpty())
127 m_baseDir = QFileInfo (globalProject["directory"].toString()).canonicalFilePath ();
128
129 /**
130 * anything changed?
131 * else be done without forced reload!
132 */
133 if (!force && (m_projectMap == globalProject))
134 return true;
135
136 /**
137 * setup global attributes in this object
138 */
139 m_projectMap = globalProject;
140
141 /**
142 * emit that we changed stuff
143 */
144 emit projectMapChanged ();
145
146 /**
147 * trigger worker to REALLY load the project model and stuff
148 */
149 QMetaObject::invokeMethod (m_worker, "loadProject", Qt::QueuedConnection, Q_ARG(QString, m_baseDir), Q_ARG(QVariantMap, m_projectMap));
150
151 /**
152 * done ok ;)
153 */
154 return true;
155}
156
157void KateProject::loadProjectDone (KateProjectSharedQStandardItem topLevel, KateProjectSharedQMapStringItem file2Item)
158{
159 /**
160 * setup model data
161 */
162 m_model.clear ();
163 m_model.invisibleRootItem()->appendColumn (topLevel->takeColumn (0));
164
165 /**
166 * setup file => item map
167 */
168 m_file2Item = file2Item;
169
170 /**
171 * readd the documents that are open atm
172 */
173 m_documentsParent = 0;
174 foreach (KTextEditor::Document *document, m_documents.keys ())
175 registerDocument (document);
176
177 /**
178 * model changed
179 */
180 emit modelChanged ();
181}
182
183void KateProject::loadIndexDone (KateProjectSharedProjectIndex projectIndex)
184{
185 /**
186 * move to our project
187 */
188 m_projectIndex = projectIndex;
189
190 /**
191 * notify external world that data is available
192 */
193 emit indexChanged ();
194}
195
196QFile *KateProject::projectLocalFile (const QString &file) const
197{
198 /**
199 * nothing on empty file names for project
200 * should not happen
201 */
202 if (m_fileName.isEmpty())
203 return 0;
204
205 /**
206 * create dir to store local files, else fail
207 */
208 if (!QDir().mkpath (m_fileName+".d"))
209 return 0;
210
211 /**
212 * try to open file read-write
213 */
214 QFile *readWriteFile = new QFile (m_fileName + ".d" + QDir::separator() + file);
215 if (!readWriteFile->open (QIODevice::ReadWrite)) {
216 delete readWriteFile;
217 return 0;
218 }
219
220 /**
221 * all fine, return file
222 */
223 return readWriteFile;
224}
225
226QTextDocument* KateProject::notesDocument ()
227{
228 /**
229 * already there?
230 */
231 if (m_notesDocument)
232 return m_notesDocument;
233
234 /**
235 * else create it
236 */
237 m_notesDocument = new QTextDocument (this);
238 m_notesDocument->setDocumentLayout (new QPlainTextDocumentLayout (m_notesDocument));
239
240 /**
241 * and load text if possible
242 */
243 if (QFile *inFile = projectLocalFile ("notes.txt")) {
244 {
245 QTextStream inStream (inFile);
246 inStream.setCodec ("UTF-8");
247 m_notesDocument->setPlainText (inStream.readAll ());
248 }
249 delete inFile;
250 }
251
252 /**
253 * and be done
254 */
255 return m_notesDocument;
256}
257
258void KateProject::saveNotesDocument ()
259{
260 /**
261 * no notes document, nothing to do
262 */
263 if (!m_notesDocument)
264 return;
265
266 /**
267 * try to get file to save to
268 */
269 if (QFile *outFile = projectLocalFile ("notes.txt")) {
270 outFile->resize (0);
271 {
272 QTextStream outStream (outFile);
273 outStream.setCodec ("UTF-8");
274 outStream << m_notesDocument->toPlainText ();
275 }
276 delete outFile;
277 }
278}
279
280
281void KateProject::slotModifiedChanged(KTextEditor::Document* document) {
282 KateProjectItem *item = itemForFile (m_documents.value (document));
283
284 if (!item) return;
285
286 item->slotModifiedChanged(document);
287}
288
289void KateProject::slotModifiedOnDisk (KTextEditor::Document *document,
290 bool isModified, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) {
291
292 KateProjectItem *item = itemForFile (m_documents.value (document));
293
294 if (!item) return;
295
296 item->slotModifiedOnDisk(document,isModified, reason);
297
298}
299
300
301void KateProject::registerDocument (KTextEditor::Document *document)
302{
303 // remember the document, if not already there
304 if (!m_documents.contains(document))
305 m_documents[document] = document->url().toLocalFile ();
306
307 // try to get item for the document
308 KateProjectItem *item = itemForFile (document->url().toLocalFile ());
309
310 // if we got one, we are done, else create a dummy!
311 if (item) {
312 disconnect(document,SIGNAL(modifiedChanged(KTextEditor::Document *)),this,SLOT(slotModifiedChanged(KTextEditor::Document *)));
313 disconnect(document,SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)),this,SLOT(slotModifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)));
314 item->slotModifiedChanged(document);
315
316/*FIXME item->slotModifiedOnDisk(document,document->isModified(),qobject_cast<KTextEditor::ModificationInterface*>(document)->modifiedOnDisk()); FIXME*/
317
318 connect(document,SIGNAL(modifiedChanged(KTextEditor::Document *)),this,SLOT(slotModifiedChanged(KTextEditor::Document *)));
319 connect(document,SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)),this,SLOT(slotModifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)));
320
321 return;
322 }
323
324 // perhaps create the parent item
325 if (!m_documentsParent) {
326 m_documentsParent = new KateProjectItem (KateProjectItem::Directory, i18n ("<untracked>"));
327 m_model.insertRow (0, m_documentsParent);
328 }
329
330 // create document item
331 QFileInfo fileInfo (document->url().toLocalFile ());
332 KateProjectItem *fileItem = new KateProjectItem (KateProjectItem::File, fileInfo.fileName());
333 fileItem->setData(document->url().toLocalFile (), Qt::ToolTipRole);
334 fileItem->slotModifiedChanged(document);
335 connect(document,SIGNAL(modifiedChanged(KTextEditor::Document *)),this,SLOT(slotModifiedChanged(KTextEditor::Document *)));
336 connect(document,SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)),this,SLOT(slotModifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)));
337
338
339 bool inserted = false;
340 for (int i = 0; i < m_documentsParent->rowCount(); ++i) {
341 if (m_documentsParent->child (i)->data(Qt::UserRole).toString() > document->url().toLocalFile ()) {
342 m_documentsParent->insertRow (i, fileItem);
343 inserted = true;
344 break;
345 }
346 }
347 if (!inserted)
348 m_documentsParent->appendRow (fileItem);
349
350 fileItem->setData (document->url().toLocalFile (), Qt::UserRole);
351 fileItem->setData (QVariant (true), Qt::UserRole + 3);
352
353 if (!m_file2Item)
354 m_file2Item = KateProjectSharedQMapStringItem (new QMap<QString, KateProjectItem *> ());
355 (*m_file2Item)[document->url().toLocalFile ()] = fileItem;
356}
357
358void KateProject::unregisterDocument (KTextEditor::Document *document)
359{
360 // skip if no works
361 if (!m_documents.contains (document))
362 return;
363
364 // perhaps kill the item we have generated
365 bool empty = false;
366 if (KateProjectItem *item = (KateProjectItem*)itemForFile (m_documents.value (document))) {
367 disconnect(document,SIGNAL(modifiedChanged(KTextEditor::Document *)),this,SLOT(slotModifiedChanged(KTextEditor::Document *)));
368 if (m_documentsParent && item->data (Qt::UserRole + 3).toBool ()) {
369 for (int i = 0; i < m_documentsParent->rowCount(); ++i) {
370 if (m_documentsParent->child (i) == item) {
371 m_documentsParent->removeRow (i);
372 break;
373 }
374 }
375
376 empty = !m_documentsParent->rowCount();
377
378 m_file2Item->remove (m_documents.value (document));
379 }
380 }
381
382 // forget the document
383 m_documents.remove (document);
384
385 // perhaps remove parent item
386 if (m_documentsParent && empty) {
387 m_model.removeRow (0);
388 m_documentsParent = 0;
389 }
390}
391
392// kate: space-indent on; indent-width 2; replace-tabs on;
393