1/* This file is part of the KDE project
2 Copyright (C) 1998, 1999, 2000 Torben Weis <weis@kde.org>
3 Copyright (C) 2004, 2010, 2012 Dag Andersen <danders@get2net.dk>
4 Copyright (C) 2006 Raphael Langerhorst <raphael.langerhorst@kdemail.net>
5 Copyright (C) 2007 Thorsten Zachmann <zachmann@kde.org>
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License as published by the Free Software Foundation; either
10 version 2 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Library General Public License for more details.
16
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to
19 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20* Boston, MA 02110-1301, USA.
21*/
22
23#include "kptmaindocument.h"
24#include "kptpart.h"
25#include "kptview.h"
26#include "kptfactory.h"
27#include "kptproject.h"
28#include "kptresource.h"
29#include "kptcontext.h"
30#include "kptschedulerpluginloader.h"
31#include "kptschedulerplugin.h"
32#include "kptbuiltinschedulerplugin.h"
33#include "kptcommand.h"
34#include "plansettings.h"
35#include "kpttask.h"
36#include "KPlatoXmlLoader.h"
37#include "kptpackage.h"
38#include "kptworkpackagemergedialog.h"
39#include "kptdebug.h"
40
41#include <KoZoomHandler.h>
42#include <KoStore.h>
43#include <KoXmlReader.h>
44#include <KoStoreDevice.h>
45#include <KoOdfReadStore.h>
46#include <KoUpdater.h>
47#include <KoProgressUpdater.h>
48#include <KoDocumentInfo.h>
49
50#include <QApplication>
51#include <QPainter>
52#include <QDir>
53#include <QMutableMapIterator>
54#include <QTreeView>
55#include <QStandardItemModel>
56
57#include <kdebug.h>
58#include <kconfig.h>
59#include <klocale.h>
60#include <kmessagebox.h>
61#include <kstandarddirs.h>
62#include <kmimetype.h>
63#include <ktemporaryfile.h>
64#include <KoGlobal.h>
65#include <kio/global.h>
66#include <kio/jobclasses.h>
67#include <kio/netaccess.h>
68#include <kio/copyjob.h>
69
70
71namespace KPlato
72{
73
74MainDocument::MainDocument(KoPart *part)
75 : KoDocument(part),
76 m_project( 0 ),
77 m_context( 0 ), m_xmlLoader(),
78 m_loadingTemplate( false ),
79 m_viewlistModified( false ),
80 m_checkingForWorkPackages( false )
81{
82 Q_ASSERT(part);
83 m_config.setReadWrite( true );
84 // Add library translation files
85 KLocale *locale = KGlobal::locale();
86 if ( locale ) {
87 locale->insertCatalog( "planlibs" );
88 locale->insertCatalog( "kdgantt" );
89 locale->insertCatalog( "timezones4" );
90#ifdef PLAN_KDEPIMLIBS_FOUND
91 locale->insertCatalog( "kabc" );
92#endif
93
94 m_config.setLocale( new KLocale( *locale ) );
95 }
96
97 loadSchedulerPlugins();
98
99 setProject( new Project( m_config ) ); // after config & plugins are loaded
100 m_project->setId( m_project->uniqueNodeId() );
101 m_project->registerNodeId( m_project ); // register myself
102
103 QTimer::singleShot ( 5000, this, SLOT(autoCheckForWorkPackages()) );
104}
105
106
107MainDocument::~MainDocument()
108{
109 qDeleteAll( m_schedulerPlugins );
110 if ( m_project ) {
111 m_project->deref(); // deletes if last user
112 }
113 qDeleteAll( m_mergedPackages );
114 delete m_context;
115}
116
117void MainDocument::setReadWrite( bool rw )
118{
119 m_config.setReadWrite( rw );
120 KoDocument::setReadWrite( rw );
121}
122
123void MainDocument::loadSchedulerPlugins()
124{
125 // Add built-in scheduler
126 addSchedulerPlugin( "Built-in", new BuiltinSchedulerPlugin( this ) );
127
128 // Add all real scheduler plugins
129 SchedulerPluginLoader *loader = new SchedulerPluginLoader(this);
130 connect(loader, SIGNAL(pluginLoaded(QString,SchedulerPlugin*)), this, SLOT(addSchedulerPlugin(QString,SchedulerPlugin*)));
131 loader->loadAllPlugins();
132}
133
134void MainDocument::addSchedulerPlugin( const QString &key, SchedulerPlugin *plugin)
135{
136 kDebug(planDbg())<<plugin;
137 m_schedulerPlugins[key] = plugin;
138}
139
140void MainDocument::configChanged()
141{
142 //m_project->setConfig( m_config );
143}
144
145void MainDocument::setProject( Project *project )
146{
147 if ( m_project ) {
148 disconnect( m_project, SIGNAL(projectChanged()), this, SIGNAL(changed()) );
149 delete m_project;
150 }
151 m_project = project;
152 if ( m_project ) {
153 connect( m_project, SIGNAL(projectChanged()), this, SIGNAL(changed()) );
154// m_project->setConfig( config() );
155 m_project->setSchedulerPlugins( m_schedulerPlugins );
156 }
157 m_aboutPage.setProject( project );
158 emit changed();
159}
160
161bool MainDocument::loadOdf( KoOdfReadStore &odfStore )
162{
163 kWarning()<< "OpenDocument not supported, let's try native xml format";
164 return loadXML( odfStore.contentDoc(), 0 ); // We have only one format, so try to load that!
165}
166
167bool MainDocument::loadXML( const KoXmlDocument &document, KoStore* )
168{
169 QPointer<KoUpdater> updater;
170 if (progressUpdater()) {
171 updater = progressUpdater()->startSubtask(1, "Plan::Part::loadXML");
172 updater->setProgress(0);
173 m_xmlLoader.setUpdater( updater );
174 }
175
176 QString value;
177 KoXmlElement plan = document.documentElement();
178
179 // Check if this is the right app
180 value = plan.attribute( "mime", QString() );
181 if ( value.isEmpty() ) {
182 kError() << "No mime type specified!";
183 setErrorMessage( i18n( "Invalid document. No mimetype specified." ) );
184 return false;
185 }
186 if ( value == "application/x-vnd.kde.kplato" ) {
187 if (updater) {
188 updater->setProgress(5);
189 }
190 m_xmlLoader.setMimetype( value );
191 QString message;
192 Project *newProject = new Project( m_config );
193 KPlatoXmlLoader loader( m_xmlLoader, newProject );
194 bool ok = loader.load( plan );
195 if ( ok ) {
196 setProject( newProject );
197 setModified( false );
198 kDebug(planDbg())<<newProject->schedules();
199 // Cleanup after possible bug:
200 // There should *not* be any deleted schedules (or with parent == 0)
201 foreach ( Node *n, newProject->nodeDict()) {
202 foreach ( Schedule *s, n->schedules()) {
203 if ( s->isDeleted() ) { // true also if parent == 0
204 kError()<<n->name()<<s;
205 n->takeSchedule( s );
206 delete s;
207 }
208 }
209 }
210 } else {
211 setErrorMessage( loader.errorMessage() );
212 delete newProject;
213 }
214 if (updater) {
215 updater->setProgress(100); // the rest is only processing, not loading
216 }
217 emit changed();
218 return ok;
219 }
220 if ( value != "application/x-vnd.kde.plan" ) {
221 kError() << "Unknown mime type " << value;
222 setErrorMessage( i18n( "Invalid document. Expected mimetype application/x-vnd.kde.plan, got %1", value ) );
223 return false;
224 }
225 QString syntaxVersion = plan.attribute( "version", PLAN_FILE_SYNTAX_VERSION );
226 m_xmlLoader.setVersion( syntaxVersion );
227 if ( syntaxVersion > PLAN_FILE_SYNTAX_VERSION ) {
228 int ret = KMessageBox::warningContinueCancel(
229 0, i18n( "This document was created with a newer version of Plan (syntax version: %1)\n"
230 "Opening it in this version of Plan will lose some information.", syntaxVersion ),
231 i18n( "File-Format Mismatch" ), KGuiItem( i18n( "Continue" ) ) );
232 if ( ret == KMessageBox::Cancel ) {
233 setErrorMessage( "USER_CANCELED" );
234 return false;
235 }
236 }
237 if (updater) updater->setProgress(5);
238/*
239#ifdef KOXML_USE_QDOM
240 int numNodes = plan.childNodes().count();
241#else
242 int numNodes = plan.childNodesCount();
243#endif
244*/
245#if 0
246This test does not work any longer. KoXml adds a couple of elements not present in the file!!
247 if ( numNodes > 2 ) {
248 //TODO: Make a proper bitching about this
249 kDebug(planDbg()) <<"*** Error ***";
250 kDebug(planDbg()) <<" Children count should be maximum 2, but is" << numNodes;
251 return false;
252 }
253#endif
254 m_xmlLoader.startLoad();
255 KoXmlNode n = plan.firstChild();
256 for ( ; ! n.isNull(); n = n.nextSibling() ) {
257 if ( ! n.isElement() ) {
258 continue;
259 }
260 KoXmlElement e = n.toElement();
261 if ( e.tagName() == "project" ) {
262 Project *newProject = new Project( m_config );
263 m_xmlLoader.setProject( newProject );
264 if ( newProject->load( e, m_xmlLoader ) ) {
265 if ( newProject->id().isEmpty() ) {
266 newProject->setId( newProject->uniqueNodeId() );
267 newProject->registerNodeId( newProject );
268 }
269 // The load went fine. Throw out the old project
270 setProject( newProject );
271 // Cleanup after possible bug:
272 // There should *not* be any deleted schedules (or with parent == 0)
273 foreach ( Node *n, newProject->nodeDict()) {
274 foreach ( Schedule *s, n->schedules()) {
275 if ( s->isDeleted() ) { // true also if parent == 0
276 kError()<<n->name()<<s;
277 n->takeSchedule( s );
278 delete s;
279 }
280 }
281 }
282 } else {
283 delete newProject;
284 m_xmlLoader.addMsg( XMLLoaderObject::Errors, "Loading of project failed" );
285 //TODO add some ui here
286 }
287 }
288 }
289 m_xmlLoader.stopLoad();
290
291 if (updater) updater->setProgress(100); // the rest is only processing, not loading
292
293 setModified( false );
294 emit changed();
295 return true;
296}
297
298QDomDocument MainDocument::saveXML()
299{
300 kDebug(planDbg());
301 QDomDocument document( "plan" );
302
303 document.appendChild( document.createProcessingInstruction(
304 "xml",
305 "version=\"1.0\" encoding=\"UTF-8\"" ) );
306
307 QDomElement doc = document.createElement( "plan" );
308 doc.setAttribute( "editor", "Plan" );
309 doc.setAttribute( "mime", "application/x-vnd.kde.plan" );
310 doc.setAttribute( "version", PLAN_FILE_SYNTAX_VERSION );
311 document.appendChild( doc );
312
313 // Save the project
314 m_project->save( doc );
315
316 return document;
317}
318
319QDomDocument MainDocument::saveWorkPackageXML( const Node *node, long id, Resource *resource )
320{
321 kDebug(planDbg());
322 QDomDocument document( "plan" );
323
324 document.appendChild( document.createProcessingInstruction(
325 "xml",
326 "version=\"1.0\" encoding=\"UTF-8\"" ) );
327
328 QDomElement doc = document.createElement( "planwork" );
329 doc.setAttribute( "editor", "Plan" );
330 doc.setAttribute( "mime", "application/x-vnd.kde.plan.work" );
331 doc.setAttribute( "version", PLANWORK_FILE_SYNTAX_VERSION );
332 doc.setAttribute( "plan-version", PLAN_FILE_SYNTAX_VERSION );
333 document.appendChild( doc );
334
335 // Work package info
336 QDomElement wp = document.createElement( "workpackage" );
337 if ( resource ) {
338 wp.setAttribute( "owner", resource->name() );
339 wp.setAttribute( "owner-id", resource->id() );
340 }
341 wp.setAttribute( "time-tag", KDateTime::currentLocalDateTime().toString( KDateTime::ISODate ) );
342 doc.appendChild( wp );
343
344 // Save the project
345 m_project->saveWorkPackageXML( doc, node, id );
346
347 return document;
348}
349
350bool MainDocument::saveWorkPackageToStream( QIODevice *dev, const Node *node, long id, Resource *resource )
351{
352 QDomDocument doc = saveWorkPackageXML( node, id, resource );
353 // Save to buffer
354 QByteArray s = doc.toByteArray(); // utf8 already
355 dev->open( QIODevice::WriteOnly );
356 int nwritten = dev->write( s.data(), s.size() );
357 if ( nwritten != (int)s.size() ) {
358 kWarning()<<"wrote:"<<nwritten<<"- expected:"<< s.size();
359 }
360 return nwritten == (int)s.size();
361}
362
363bool MainDocument::saveWorkPackageFormat( const QString &file, const Node *node, long id, Resource *resource )
364{
365 kDebug(planDbg()) <<"Saving to store";
366
367 KoStore::Backend backend = KoStore::Zip;
368#ifdef QCA2
369/* if ( d->m_specialOutputFlag == SaveEncrypted ) {
370 backend = KoStore::Encrypted;
371 kDebug(planDbg()) <<"Saving using encrypted backend.";
372 }*/
373#endif
374
375 QByteArray mimeType = "application/x-vnd.kde.plan.work";
376 kDebug(planDbg()) <<"MimeType=" << mimeType;
377
378 KoStore *store = KoStore::createStore( file, KoStore::Write, mimeType, backend );
379/* if ( d->m_specialOutputFlag == SaveEncrypted && !d->m_password.isNull( ) ) {
380 store->setPassword( d->m_password );
381 }*/
382 if ( store->bad() ) {
383 setErrorMessage( i18n( "Could not create the workpackage file for saving: %1", file ) ); // more details needed?
384 delete store;
385 return false;
386 }
387 // Tell KoStore not to touch the file names
388
389
390 if ( ! store->open( "root" ) ) {
391 setErrorMessage( i18n( "Not able to write '%1'. Partition full?", QString( "maindoc.xml") ) );
392 delete store;
393 return false;
394 }
395 KoStoreDevice dev( store );
396 if ( !saveWorkPackageToStream( &dev, node, id, resource ) || !store->close() ) {
397 kDebug(planDbg()) <<"saveToStream failed";
398 delete store;
399 return false;
400 }
401 node->documents().saveToStore( store );
402
403 kDebug(planDbg()) <<"Saving done of url:" << file;
404 if ( !store->finalize() ) {
405 delete store;
406 return false;
407 }
408 // Success
409 delete store;
410
411 return true;
412}
413
414bool MainDocument::saveWorkPackageUrl( const KUrl &_url, const Node *node, long id, Resource *resource )
415{
416 //kDebug(planDbg())<<_url;
417 QApplication::setOverrideCursor( Qt::WaitCursor );
418 emit statusBarMessage( i18n("Saving...") );
419 bool ret = false;
420 ret = saveWorkPackageFormat( _url.path(), node, id, resource ); // kzip don't handle file://
421 QApplication::restoreOverrideCursor();
422 emit clearStatusBarMessage();
423 return ret;
424}
425
426bool MainDocument::loadWorkPackage( Project &project, const KUrl &url )
427{
428 kDebug(planDbg())<<url;
429 if ( ! url.isLocalFile() ) {
430 kDebug(planDbg())<<"TODO: download if url not local";
431 return false;
432 }
433 KoStore *store = KoStore::createStore( url.path(), KoStore::Read, "", KoStore::Auto );
434 if ( store->bad() ) {
435// d->lastErrorMessage = i18n( "Not a valid Calligra file: %1", file );
436 kDebug(planDbg())<<"bad store"<<url.prettyUrl();
437 delete store;
438// QApplication::restoreOverrideCursor();
439 return false;
440 }
441 if ( ! store->open( "root" ) ) { // "old" file format (maindoc.xml)
442 // i18n( "File does not have a maindoc.xml: %1", file );
443 kDebug(planDbg())<<"No root"<<url.prettyUrl();
444 delete store;
445// QApplication::restoreOverrideCursor();
446 return false;
447 }
448 Package *package = 0;
449 KoXmlDocument doc;
450 QString errorMsg; // Error variables for QDomDocument::setContent
451 int errorLine, errorColumn;
452 bool ok = doc.setContent( store->device(), &errorMsg, &errorLine, &errorColumn );
453 if ( ! ok ) {
454 kError() << "Parsing error in " << url.url() << "! Aborting!" << endl
455 << " In line: " << errorLine << ", column: " << errorColumn << endl
456 << " Error message: " << errorMsg;
457 //d->lastErrorMessage = i18n( "Parsing error in %1 at line %2, column %3\nError message: %4",filename ,errorLine, errorColumn , QCoreApplication::translate("QXml", errorMsg.toUtf8(), 0, QCoreApplication::UnicodeUTF8));
458 } else {
459 package = loadWorkPackageXML( project, store->device(), doc, url );
460 if ( package ) {
461 package->url = url;
462 m_workpackages.insert( package->timeTag, package );
463 } else {
464 ok = false;
465 }
466 }
467 store->close();
468 //###
469 if ( ok && package && package->settings.documents ) {
470 ok = extractFiles( store, package );
471 }
472 delete store;
473 if ( ! ok ) {
474// QApplication::restoreOverrideCursor();
475 return false;
476 }
477 return true;
478}
479
480Package *MainDocument::loadWorkPackageXML( Project &project, QIODevice *, const KoXmlDocument &document, const KUrl &/*url*/ )
481{
482 QString value;
483 bool ok = true;
484 Project *proj = 0;
485 Package *package = 0;
486 KoXmlElement plan = document.documentElement();
487
488 // Check if this is the right app
489 value = plan.attribute( "mime", QString() );
490 if ( value.isEmpty() ) {
491 kDebug(planDbg()) << "No mime type specified!";
492 setErrorMessage( i18n( "Invalid document. No mimetype specified." ) );
493 return 0;
494 } else if ( value == "application/x-vnd.kde.kplato.work" ) {
495 m_xmlLoader.setMimetype( value );
496 m_xmlLoader.setWorkVersion( plan.attribute( "version", "0.0.0" ) );
497 proj = new Project( m_config );
498 KPlatoXmlLoader loader( m_xmlLoader, proj );
499 ok = loader.loadWorkpackage( plan );
500 if ( ! ok ) {
501 setErrorMessage( loader.errorMessage() );
502 delete proj;
503 return 0;
504 }
505 package = loader.package();
506 package->timeTag = KDateTime::fromString( loader.timeTag(), KDateTime::ISODate );
507 } else if ( value != "application/x-vnd.kde.plan.work" ) {
508 kDebug(planDbg()) << "Unknown mime type " << value;
509 setErrorMessage( i18n( "Invalid document. Expected mimetype application/x-vnd.kde.plan.work, got %1", value ) );
510 return 0;
511 } else {
512 QString syntaxVersion = plan.attribute( "version", "0.0.0" );
513 m_xmlLoader.setWorkVersion( syntaxVersion );
514 if ( syntaxVersion > PLANWORK_FILE_SYNTAX_VERSION ) {
515 int ret = KMessageBox::warningContinueCancel(
516 0, i18n( "This document was created with a newer version of PlanWork (syntax version: %1)\n"
517 "Opening it in this version of PlanWork will lose some information.", syntaxVersion ),
518 i18n( "File-Format Mismatch" ), KGuiItem( i18n( "Continue" ) ) );
519 if ( ret == KMessageBox::Cancel ) {
520 setErrorMessage( "USER_CANCELED" );
521 return 0;
522 }
523 }
524 m_xmlLoader.setVersion( plan.attribute( "plan-version", PLAN_FILE_SYNTAX_VERSION ) );
525 m_xmlLoader.startLoad();
526 proj = new Project();
527 package = new Package();
528 package->project = proj;
529 KoXmlNode n = plan.firstChild();
530 for ( ; ! n.isNull(); n = n.nextSibling() ) {
531 if ( ! n.isElement() ) {
532 continue;
533 }
534 KoXmlElement e = n.toElement();
535 if ( e.tagName() == "project" ) {
536 m_xmlLoader.setProject( proj );
537 ok = proj->load( e, m_xmlLoader );
538 if ( ! ok ) {
539 m_xmlLoader.addMsg( XMLLoaderObject::Errors, "Loading of work package failed" );
540 //TODO add some ui here
541 }
542 } else if ( e.tagName() == "workpackage" ) {
543 package->timeTag = KDateTime::fromString( e.attribute( "time-tag" ), KDateTime::ISODate );
544 package->ownerId = e.attribute( "owner-id" );
545 package->ownerName = e.attribute( "owner" );
546 kDebug(planDbg())<<"workpackage:"<<package->timeTag<<package->ownerId<<package->ownerName;
547 KoXmlElement elem;
548 forEachElement( elem, e ) {
549 if ( elem.tagName() != "settings" ) {
550 continue;
551 }
552 package->settings.usedEffort = (bool)elem.attribute( "used-effort" ).toInt();
553 package->settings.progress = (bool)elem.attribute( "progress" ).toInt();
554 package->settings.documents = (bool)elem.attribute( "documents" ).toInt();
555 }
556 }
557 }
558 if ( proj->numChildren() > 0 ) {
559 package->task = static_cast<Task*>( proj->childNode( 0 ) );
560 package->toTask = qobject_cast<Task*>( m_project->findNode( package->task->id() ) );
561 WorkPackage &wp = package->task->workPackage();
562 if ( wp.ownerId().isEmpty() ) {
563 wp.setOwnerId( package->ownerId );
564 wp.setOwnerName( package->ownerName );
565 }
566 kDebug(planDbg())<<"Task set:"<<package->task->name();
567 }
568 m_xmlLoader.stopLoad();
569 }
570 if ( ok && proj->id() == project.id() && proj->childNode( 0 ) ) {
571 ok = project.nodeDict().contains( proj->childNode( 0 )->id() );
572 if ( ok && m_mergedPackages.contains( package->timeTag ) ) {
573 ok = false; // already merged
574 }
575 if ( ok && package->timeTag.isValid() && ! m_mergedPackages.contains( package->timeTag ) ) {
576 m_mergedPackages[ package->timeTag ] = proj; // register this for next time
577 }
578 if ( ok && ! package->timeTag.isValid() ) {
579 kWarning()<<"Work package is not time tagged:"<<proj->childNode( 0 )->name()<<package->url;
580 ok = false;
581 }
582 }
583 if ( ! ok ) {
584 delete proj;
585 delete package;
586 return 0;
587 }
588 Q_ASSERT( package );
589 return package;
590}
591
592bool MainDocument::extractFiles( KoStore *store, Package *package )
593{
594 if ( package->task == 0 ) {
595 kError()<<"No task!";
596 return false;
597 }
598 foreach ( Document *doc, package->task->documents().documents() ) {
599 if ( ! doc->isValid() || doc->type() != Document::Type_Product || doc->sendAs() != Document::SendAs_Copy ) {
600 continue;
601 }
602 if ( ! extractFile( store, package, doc ) ) {
603 return false;
604 }
605 }
606 return true;
607}
608
609bool MainDocument::extractFile( KoStore *store, Package *package, const Document *doc )
610{
611 KTemporaryFile tmpfile;
612 if ( ! tmpfile.open() ) {
613 kError()<<"Failed to open temporary file";
614 return false;
615 }
616 if ( ! store->extractFile( doc->url().fileName(), tmpfile.fileName() ) ) {
617 kError()<<"Failed to extract file:"<<doc->url().fileName()<<"to:"<<tmpfile.fileName();
618 return false;
619 }
620 package->documents.insert( tmpfile.fileName(), doc->url() );
621 tmpfile.setAutoRemove( false );
622 kDebug(planDbg())<<"extracted:"<<doc->url().fileName()<<"->"<<tmpfile.fileName();
623 return true;
624}
625
626void MainDocument::autoCheckForWorkPackages()
627{
628 if ( m_config.checkForWorkPackages() ) {
629 checkForWorkPackages( true );
630 }
631 QTimer::singleShot ( 10000, this, SLOT(autoCheckForWorkPackages()) );
632}
633
634void MainDocument::checkForWorkPackages( bool keep )
635{
636 if ( m_checkingForWorkPackages || m_config.retrieveUrl().isEmpty() || m_project == 0 || m_project->numChildren() == 0 ) {
637 return;
638 }
639 m_checkingForWorkPackages = true;
640 if ( ! keep ) {
641 qDeleteAll( m_mergedPackages );
642 m_mergedPackages.clear();
643 }
644 QDir dir( m_config.retrieveUrl().path(), "*.planwork" );
645 m_infoList = dir.entryInfoList( QDir::Files | QDir::Readable, QDir::Time );
646 checkForWorkPackage();
647 return;
648}
649
650void MainDocument::checkForWorkPackage()
651{
652 if ( ! m_infoList.isEmpty() ) {
653 loadWorkPackage( *m_project, KUrl( m_infoList.takeLast().absoluteFilePath() ) );
654 if ( ! m_infoList.isEmpty() ) {
655 QTimer::singleShot ( 0, this, SLOT(checkForWorkPackage()) );
656 return;
657 }
658 // all files read
659 // remove other projects
660 QMutableMapIterator<KDateTime, Package*> it( m_workpackages );
661 while ( it.hasNext() ) {
662 it.next();
663 Package *package = it.value();
664 if ( package->project->id() != m_project->id() ) {
665 delete package->project;
666 delete package;
667 it.remove();
668 }
669 }
670 // Merge our workpackages
671 if ( ! m_workpackages.isEmpty() ) {
672 WorkPackageMergeDialog *dlg = new WorkPackageMergeDialog( i18n( "New work packages detected. Merge data with existing tasks?" ), m_workpackages );
673 connect(dlg, SIGNAL(finished(int)), SLOT(workPackageMergeDialogFinished(int)));
674 dlg->show();
675 dlg->raise();
676 dlg->activateWindow();
677 }
678 }
679}
680
681void MainDocument::workPackageMergeDialogFinished( int result )
682{
683 WorkPackageMergeDialog *dlg = qobject_cast<WorkPackageMergeDialog*>( sender() );
684 if ( dlg == 0 ) {
685 return;
686 }
687 if ( result == KDialog::Yes ) {
688 // merge the oldest first
689 foreach( int i, dlg->checkedList() ) {
690 mergeWorkPackage( m_workpackages.values().at( i ) );
691 }
692 // 'Yes' was hit so terminate all packages
693 foreach( const Package *p, m_workpackages.values() ) {
694 terminateWorkPackage( p );
695 }
696 }
697 qDeleteAll( m_workpackages );
698 m_workpackages.clear();
699 m_checkingForWorkPackages = false;
700 dlg->deleteLater();
701}
702
703void MainDocument::mergeWorkPackages()
704{
705 foreach ( Package *package, m_workpackages ) {
706 mergeWorkPackage( package );
707 }
708}
709
710void MainDocument::terminateWorkPackage( const Package *package )
711{
712 QFile file( package->url.path() );
713 if ( ! file.exists() ) {
714 return;
715 }
716 if ( KPlatoSettings::deleteFile() || KPlatoSettings::saveUrl().isEmpty() ) {
717 file.remove();
718 } else if ( KPlatoSettings::saveFile() && ! KPlatoSettings::saveUrl().isEmpty() ) {
719 QDir dir( KPlatoSettings::saveUrl().path() );
720 if ( ! dir.exists() ) {
721 if ( ! dir.mkpath( dir.path() ) ) {
722 //TODO message
723 kDebug(planDbg())<<"Could not create directory:"<<dir.path();
724 return;
725 }
726 }
727 QFileInfo from( file );
728 QString name = KPlatoSettings::saveUrl().path() + '/' + from.fileName();
729 if ( file.rename( name ) ) {
730 return;
731 }
732 name = KPlatoSettings::saveUrl().path() + '/';
733 name += from.completeBaseName() + "-%1";
734 if ( ! from.suffix().isEmpty() ) {
735 name += '.' + from.suffix();
736 }
737 int i = 0;
738 bool ok = false;
739 while ( ! ok && i < 1000 ) {
740 ++i;
741 ok = QFile::rename( file.fileName(), name.arg( i ) );
742 }
743 if ( ! ok ) {
744 //TODO message
745 kDebug(planDbg())<<"terminateWorkPackage: Failed to save"<<file.fileName();
746 }
747 }
748}
749
750void MainDocument::mergeWorkPackage( const Package *package )
751{
752 const Project &proj = *(package->project);
753 if ( proj.id() == m_project->id() && proj.childNode( 0 ) ) {
754 const Task *from = package->task;
755 Task *to = package->toTask;
756 if ( to && from ) {
757 mergeWorkPackage( to, from, package );
758 }
759 }
760}
761
762void MainDocument::mergeWorkPackage( Task *to, const Task *from, const Package *package )
763{
764 Resource *resource = m_project->findResource( package->ownerId );
765 if ( resource == 0 ) {
766 KMessageBox::error( 0, i18n( "The package owner '%1' is not a resource in this project. You must handle this manually.", package->ownerName ) );
767 return;
768 }
769
770 MacroCommand *cmd = new MacroCommand( kundo2_noi18n("Merge workpackage") );
771 Completion &org = to->completion();
772 const Completion &curr = from->completion();
773
774 if ( package->settings.progress ) {
775 if ( org.isStarted() != curr.isStarted() ) {
776 cmd->addCommand( new ModifyCompletionStartedCmd(org, curr.isStarted() ) );
777 }
778 if ( org.isFinished() != curr.isFinished() ) {
779 cmd->addCommand( new ModifyCompletionFinishedCmd( org, curr.isFinished() ) );
780 }
781 if ( org.startTime() != curr.startTime() ) {
782 cmd->addCommand( new ModifyCompletionStartTimeCmd( org, curr.startTime() ) );
783 }
784 if ( org.finishTime() != curr.finishTime() ) {
785 cmd->addCommand( new ModifyCompletionFinishTimeCmd( org, curr.finishTime() ) );
786 }
787 // TODO: review how/if to merge data from different resources
788 // remove entries
789 foreach ( const QDate &d, org.entries().keys() ) {
790 if ( ! curr.entries().contains( d ) ) {
791 kDebug(planDbg())<<"remove entry "<<d;
792 cmd->addCommand( new RemoveCompletionEntryCmd( org, d ) );
793 }
794 }
795 // add new entries / modify existing
796 foreach ( const QDate &d, curr.entries().keys() ) {
797 if ( org.entries().contains( d ) && curr.entry( d ) == org.entry( d ) ) {
798 continue;
799 }
800 Completion::Entry *e = new Completion::Entry( *( curr.entry( d ) ) );
801 cmd->addCommand( new ModifyCompletionEntryCmd( org, d, e ) );
802 }
803 }
804 if ( package->settings.usedEffort ) {
805 Completion::UsedEffort *ue = new Completion::UsedEffort();
806 Completion::Entry prev;
807 foreach ( const QDate &d, curr.entries().keys() ) {
808 Completion::Entry e = *( curr.entry( d ) );
809 // set used effort from date entry and remove used effort from date entry
810 Completion::UsedEffort::ActualEffort effort( e.totalPerformed - prev.totalPerformed );
811 ue->setEffort( d, effort );
812 prev = e;
813 }
814 cmd->addCommand( new AddCompletionUsedEffortCmd( org, resource, ue ) );
815 }
816 bool docsaved = false;
817 if ( package->settings.documents ) {
818 //TODO: handle remote files
819 QMap<QString, KUrl>::const_iterator it = package->documents.constBegin();
820 QMap<QString, KUrl>::const_iterator end = package->documents.constEnd();
821 for ( ; it != end; ++it ) {
822 KUrl src( it.key() );
823 KIO::Job *job = KIO::move( src, it.value(), KIO::Overwrite );
824 if ( KIO::NetAccess::synchronousRun( job, 0 ) ) {
825 docsaved = true;
826 //TODO: async
827 kDebug(planDbg())<<"Moved file:"<<src<<it.value();
828 }
829 }
830 }
831 if ( ! docsaved && cmd->isEmpty() ) {
832 KMessageBox::information( 0, i18n( "Nothing to save from this package" ) );
833 }
834 // add a copy to our tasks list of transmitted packages
835 WorkPackage *wp = new WorkPackage( from->workPackage() );
836 wp->setParentTask( to );
837 if ( ! wp->transmitionTime().isValid() ) {
838 wp->setTransmitionTime( package->timeTag );
839 }
840 wp->setTransmitionStatus( WorkPackage::TS_Receive );
841 cmd->addCommand( new WorkPackageAddCmd( m_project, to, wp ) );
842 addCommand( cmd );
843}
844
845void MainDocument::paintContent( QPainter &, const QRect &)
846{
847 // Don't embed this app!!!
848}
849
850void MainDocument::slotViewDestroyed()
851{
852}
853
854void MainDocument::setLoadingTemplate(bool loading)
855{
856 m_loadingTemplate = loading;
857}
858
859bool MainDocument::completeLoading( KoStore *store )
860{
861 // If we get here the new project is loaded and set
862 if ( m_loadingTemplate ) {
863 //kDebug(planDbg())<<"Loading template, generate unique ids";
864 m_project->generateUniqueIds();
865 m_project->setConstraintStartTime( KDateTime( KDateTime::currentLocalDateTime().date(), QTime( 0, 0, 0 ) ) );
866 m_project->setConstraintEndTime( m_project->constraintStartTime().addYears( 2 ) );
867 } else if ( isImporting() ) {
868 // NOTE: I don't think this is a good idea.
869 // Let the filter generate ids for non-plan files.
870 // If the user wants to create a new project from an old one,
871 // he should use Tools -> Insert Project File
872
873 //m_project->generateUniqueNodeIds();
874 }
875 if ( store == 0 ) {
876 // can happen if loading a template
877 kDebug(planDbg())<<"No store";
878 return true; // continue anyway
879 }
880 delete m_context;
881 m_context = new Context();
882 KoXmlDocument doc;
883 if ( loadAndParse( store, "context.xml", doc ) ) {
884 store->close();
885 m_context->load( doc );
886 } else kWarning()<<"No context";
887 return true;
888}
889
890// TODO:
891// Due to splitting of KoDocument into a document and a part,
892// we simmulate the old behaviour by registering all views in the document.
893// Find a better solution!
894void MainDocument::registerView( View* view )
895{
896 if ( view && ! m_views.contains( view ) ) {
897 m_views << QPointer<View>( view );
898 }
899}
900
901bool MainDocument::completeSaving( KoStore *store )
902{
903 foreach ( View *view, m_views ) {
904 if ( view ) {
905 if ( store->open( "context.xml" ) ) {
906 if ( m_context == 0 ) m_context = new Context();
907 QDomDocument doc = m_context->save( view );
908
909 KoStoreDevice dev( store );
910 QByteArray s = doc.toByteArray(); // this is already Utf8!
911 (void)dev.write( s.data(), s.size() );
912 (void)store->close();
913
914 m_viewlistModified = false;
915 emit viewlistModified( false );
916 }
917 break;
918 }
919 }
920 return true;
921}
922
923bool MainDocument::loadAndParse(KoStore *store, const QString &filename, KoXmlDocument &doc)
924{
925 //kDebug(planDbg()) << "oldLoadAndParse: Trying to open " << filename;
926
927 if (!store->open(filename))
928 {
929 kWarning() << "Entry " << filename << " not found!";
930// d->lastErrorMessage = i18n( "Could not find %1",filename );
931 return false;
932 }
933 // Error variables for QDomDocument::setContent
934 QString errorMsg;
935 int errorLine, errorColumn;
936 bool ok = doc.setContent( store->device(), &errorMsg, &errorLine, &errorColumn );
937 if ( !ok )
938 {
939 kError() << "Parsing error in " << filename << "! Aborting!" << endl
940 << " In line: " << errorLine << ", column: " << errorColumn << endl
941 << " Error message: " << errorMsg;
942/* d->lastErrorMessage = i18n( "Parsing error in %1 at line %2, column %3\nError message: %4"
943 ,filename ,errorLine, errorColumn ,
944 QCoreApplication::translate("QXml", errorMsg.toUtf8(), 0,
945 QCoreApplication::UnicodeUTF8));*/
946 store->close();
947 return false;
948 }
949 kDebug(planDbg()) << "File " << filename << " loaded and parsed";
950 return true;
951}
952
953void MainDocument::insertFile( const QString &filename, Node *parent, Node *after )
954{
955 Part *part = new Part( this );
956 MainDocument *doc = new MainDocument( part );
957 part->setDocument( doc );
958 doc->disconnect(); // doc shall not handle feedback from openUrl()
959 doc->setAutoSave( 0 ); //disable
960 doc->m_insertFileInfo.url = filename;
961 doc->m_insertFileInfo.parent = parent;
962 doc->m_insertFileInfo.after = after;
963 connect(doc, SIGNAL(completed()), SLOT(insertFileCompleted()));
964 connect(doc, SIGNAL(canceled(QString)), SLOT(insertFileCancelled(QString)));
965
966 doc->openUrl( KUrl( filename ) );
967}
968
969void MainDocument::insertFileCompleted()
970{
971 kDebug(planDbg())<<sender();
972 Part *part = qobject_cast<Part*>( sender() );
973 if ( part ) {
974 MainDocument *doc = qobject_cast<MainDocument*>( part->document() );
975 if ( doc ) {
976 Project &p = doc->getProject();
977 insertProject( p, doc->m_insertFileInfo.parent, doc->m_insertFileInfo.after );
978 } else {
979 KMessageBox::error( 0, i18n("Internal error, failed to insert file.") );
980 }
981 part->deleteLater(); // also deletes document
982 }
983}
984
985void MainDocument::insertFileCancelled( const QString &error )
986{
987 kDebug(planDbg())<<sender()<<"error="<<error;
988 if ( ! error.isEmpty() ) {
989 KMessageBox::error( 0, error );
990 }
991 Part *part = qobject_cast<Part*>( sender() );
992 if ( part ) {
993 part->deleteLater(); // also deletes document
994 }
995}
996
997bool MainDocument::insertProject( Project &project, Node *parent, Node *after )
998{
999 // make sure node ids in new project is unique also in old project
1000 QList<QString> existingIds = m_project->nodeDict().keys();
1001 foreach ( Node *n, project.allNodes() ) {
1002 QString oldid = n->id();
1003 n->setId( project.uniqueNodeId( existingIds ) );
1004 project.removeId( oldid ); // remove old id
1005 project.registerNodeId( n ); // register new id
1006 }
1007 MacroCommand *m = new InsertProjectCmd( project, parent==0?m_project:parent, after, kundo2_i18n( "Insert project" ) );
1008 if ( m->isEmpty() ) {
1009 delete m;
1010 } else {
1011 addCommand( m );
1012 }
1013 return true;
1014}
1015
1016void MainDocument::insertViewListItem( View */*view*/, const ViewListItem *item, const ViewListItem *parent, int index )
1017{
1018 // FIXME callers should take care that they now get a signal even if originating from themselves
1019 emit viewListItemAdded(item, parent, index);
1020 setModified( true );
1021 m_viewlistModified = true;
1022}
1023
1024void MainDocument::removeViewListItem( View */*view*/, const ViewListItem *item )
1025{
1026 // FIXME callers should take care that they now get a signal even if originating from themselves
1027 emit viewListItemRemoved(item);
1028 setModified( true );
1029 m_viewlistModified = true;
1030}
1031
1032void MainDocument::setModified( bool mod )
1033{
1034 kDebug(planDbg())<<mod<<m_viewlistModified;
1035 KoDocument::setModified( mod || m_viewlistModified ); // Must always call to activate autosave
1036}
1037
1038void MainDocument::viewlistModified()
1039{
1040 if ( ! m_viewlistModified ) {
1041 m_viewlistModified = true;
1042 }
1043 setModified( true ); // Must always call to activate autosave
1044}
1045
1046void MainDocument::createNewProject()
1047{
1048 setEmpty();
1049 clearUndoHistory();
1050 setModified( false );
1051 resetURL();
1052 KoDocumentInfo *info = documentInfo();
1053 info->resetMetaData();
1054 info->setProperty( "title", "" );
1055 setTitleModified();
1056
1057 m_project->generateUniqueIds();
1058 Duration dur = m_project->constraintEndTime() - m_project->constraintStartTime();
1059 m_project->setConstraintStartTime( KDateTime( KDateTime::currentLocalDateTime().date(), QTime( 0, 0, 0 ) ) );
1060 m_project->setConstraintEndTime( m_project->constraintStartTime() + dur );
1061
1062 while ( m_project->numScheduleManagers() > 0 ) {
1063 foreach ( ScheduleManager *sm, m_project->allScheduleManagers() ) {
1064 if ( sm->childCount() > 0 ) {
1065 continue;
1066 }
1067 if ( sm->expected() ) {
1068 sm->expected()->setDeleted( true );
1069 sm->setExpected( 0 );
1070 }
1071 m_project->takeScheduleManager( sm );
1072 delete sm;
1073 }
1074 }
1075 foreach ( Schedule *s, m_project->schedules() ) {
1076 m_project->takeSchedule( s );
1077 delete s;
1078 }
1079 foreach ( Node *n, m_project->allNodes() ) {
1080 foreach ( Schedule *s, n->schedules() ) {
1081 n->takeSchedule( s );
1082 delete s;
1083 }
1084 }
1085 foreach ( Resource *r, m_project->resourceList() ) {
1086 foreach ( Schedule *s, r->schedules().values() ) {
1087 r->takeSchedule( s );
1088 delete s;
1089 }
1090 }
1091}
1092
1093} //KPlato namespace
1094
1095#include "kptmaindocument.moc"
1096