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 | |
71 | namespace KPlato |
72 | { |
73 | |
74 | MainDocument::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 | |
107 | MainDocument::~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 | |
117 | void MainDocument::setReadWrite( bool rw ) |
118 | { |
119 | m_config.setReadWrite( rw ); |
120 | KoDocument::setReadWrite( rw ); |
121 | } |
122 | |
123 | void 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 | |
134 | void MainDocument::addSchedulerPlugin( const QString &key, SchedulerPlugin *plugin) |
135 | { |
136 | kDebug(planDbg())<<plugin; |
137 | m_schedulerPlugins[key] = plugin; |
138 | } |
139 | |
140 | void MainDocument::configChanged() |
141 | { |
142 | //m_project->setConfig( m_config ); |
143 | } |
144 | |
145 | void 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 | |
161 | bool 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 | |
167 | bool 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 |
246 | This 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 | |
298 | QDomDocument 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 | |
319 | QDomDocument 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 | |
350 | bool 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 | |
363 | bool 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 | |
414 | bool 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 | |
426 | bool 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 | |
480 | Package *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 | |
592 | bool 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 | |
609 | bool 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 | |
626 | void MainDocument::autoCheckForWorkPackages() |
627 | { |
628 | if ( m_config.checkForWorkPackages() ) { |
629 | checkForWorkPackages( true ); |
630 | } |
631 | QTimer::singleShot ( 10000, this, SLOT(autoCheckForWorkPackages()) ); |
632 | } |
633 | |
634 | void 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 | |
650 | void 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 | |
681 | void 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 | |
703 | void MainDocument::mergeWorkPackages() |
704 | { |
705 | foreach ( Package *package, m_workpackages ) { |
706 | mergeWorkPackage( package ); |
707 | } |
708 | } |
709 | |
710 | void 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 | |
750 | void 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 | |
762 | void 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 | |
845 | void MainDocument::paintContent( QPainter &, const QRect &) |
846 | { |
847 | // Don't embed this app!!! |
848 | } |
849 | |
850 | void MainDocument::slotViewDestroyed() |
851 | { |
852 | } |
853 | |
854 | void MainDocument::setLoadingTemplate(bool loading) |
855 | { |
856 | m_loadingTemplate = loading; |
857 | } |
858 | |
859 | bool 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! |
894 | void MainDocument::registerView( View* view ) |
895 | { |
896 | if ( view && ! m_views.contains( view ) ) { |
897 | m_views << QPointer<View>( view ); |
898 | } |
899 | } |
900 | |
901 | bool 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 | |
923 | bool 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 | |
953 | void 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 | |
969 | void 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 | |
985 | void 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 | |
997 | bool 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 | |
1016 | void 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 | |
1024 | void 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 | |
1032 | void 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 | |
1038 | void MainDocument::viewlistModified() |
1039 | { |
1040 | if ( ! m_viewlistModified ) { |
1041 | m_viewlistModified = true; |
1042 | } |
1043 | setModified( true ); // Must always call to activate autosave |
1044 | } |
1045 | |
1046 | void 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 | |