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  * Copyright (C) 2019 Dag Andersen <danders@get2net.dk>
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 // clazy:excludeall=qstring-arg
25 #include "kptmaindocument.h"
26 #include "kptpart.h"
27 #include "kptview.h"
28 #include "kptfactory.h"
29 #include "kptproject.h"
30 #include "kptlocale.h"
31 #include "kptresource.h"
32 #include "kptcontext.h"
33 #include "kptschedulerpluginloader.h"
34 #include "kptschedulerplugin.h"
35 #include "kptbuiltinschedulerplugin.h"
36 #include "kptschedule.h"
37 #include "kptcommand.h"
38 #include "calligraplansettings.h"
39 #include "kpttask.h"
40 #include "KPlatoXmlLoader.h"
41 #include "XmlSaveContext.h"
42 #include "kptpackage.h"
43 #include "kptdebug.h"
44 
45 #include <KoStore.h>
46 #include <KoXmlReader.h>
47 #include <KoStoreDevice.h>
48 #include <KoOdfReadStore.h>
49 #include <KoUpdater.h>
50 #include <KoProgressUpdater.h>
51 #include <KoDocumentInfo.h>
52 
53 #include <QApplication>
54 #include <QPainter>
55 #include <QDir>
56 #include <QMutableMapIterator>
57 #include <QTemporaryFile>
58 
59 #include <klocalizedstring.h>
60 #include <kmessagebox.h>
61 #include <KIO/CopyJob>
62 #include <KDirWatch>
63 
64 #include <kundo2command.h>
65 
66 #ifdef HAVE_KHOLIDAYS
67 #include <KHolidays/HolidayRegion>
68 #endif
69 
70 namespace KPlato
71 {
72 
MainDocument(KoPart * part)73 MainDocument::MainDocument(KoPart *part)
74         : KoDocument(part),
75         m_project(0),
76         m_context(0), m_xmlLoader(),
77         m_loadingTemplate(false),
78         m_loadingSharedResourcesTemplate(false),
79         m_viewlistModified(false),
80         m_checkingForWorkPackages(false),
81         m_loadingSharedProject(false),
82         m_skipSharedProjects(false),
83         m_isLoading(false),
84         m_isTaskModule(false),
85         m_calculationCommand(nullptr),
86         m_currentCalculationManager(nullptr),
87         m_nextCalculationManager(nullptr),
88         m_taskModulesWatch(nullptr)
89 {
90     Q_ASSERT(part);
91     setAlwaysAllowSaving(true);
92     m_config.setReadWrite(isReadWrite());
93 
94     loadSchedulerPlugins();
95 
96     setProject(new Project(m_config)); // after config & plugins are loaded
97     m_project->setId(m_project->uniqueNodeId());
98     m_project->registerNodeId(m_project); // register myself
99 
100     connect(this, &MainDocument::insertSharedProject, this, &MainDocument::slotInsertSharedProject);
101 }
102 
103 
~MainDocument()104 MainDocument::~MainDocument()
105 {
106     qDeleteAll(m_schedulerPlugins);
107     if (m_project) {
108         m_project->deref(); // deletes if last user
109     }
110     qDeleteAll(m_mergedPackages);
111     delete m_context;
112     delete m_calculationCommand;
113 }
114 
initEmpty()115 void MainDocument::initEmpty()
116 {
117     KoDocument::initEmpty();
118     setProject(new Project(m_config));
119 }
120 
slotNodeChanged(Node * node,int property)121 void MainDocument::slotNodeChanged(Node *node, int property)
122 {
123     switch (property) {
124         case Node::TypeProperty:
125         case Node::ResourceRequestProperty:
126         case Node::ConstraintTypeProperty:
127         case Node::StartConstraintProperty:
128         case Node::EndConstraintProperty:
129         case Node::PriorityProperty:
130         case Node::EstimateProperty:
131         case Node::EstimateRiskProperty:
132             setCalculationNeeded();
133             break;
134         case Node::EstimateOptimisticProperty:
135         case Node::EstimatePessimisticProperty:
136             if (node->estimate()->risktype() != Estimate::Risk_None) {
137                 setCalculationNeeded();
138             }
139             break;
140         default:
141             break;
142     }
143 }
144 
slotScheduleManagerChanged(ScheduleManager * sm,int property)145 void MainDocument::slotScheduleManagerChanged(ScheduleManager *sm, int property)
146 {
147     if (sm->schedulingMode() == ScheduleManager::AutoMode) {
148         switch (property) {
149             case ScheduleManager::DirectionProperty:
150             case ScheduleManager::OverbookProperty:
151             case ScheduleManager::DistributionProperty:
152             case ScheduleManager::SchedulingModeProperty:
153             case ScheduleManager::GranularityProperty:
154                 setCalculationNeeded();
155                 break;
156             default:
157                 break;
158         }
159     }
160 }
161 
setCalculationNeeded()162 void MainDocument::setCalculationNeeded()
163 {
164     for (ScheduleManager *sm : m_project->allScheduleManagers()) {
165         if (sm->isBaselined()) {
166             continue;
167         }
168         if (sm->schedulingMode() == ScheduleManager::AutoMode) {
169             m_nextCalculationManager = sm;
170             break;
171         }
172     }
173     if (!m_currentCalculationManager) {
174         m_currentCalculationManager = m_nextCalculationManager;
175         m_nextCalculationManager = nullptr;
176 
177         QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
178     }
179 }
180 
slotStartCalculation()181 void MainDocument::slotStartCalculation()
182 {
183     if (m_currentCalculationManager) {
184         m_calculationCommand = new CalculateScheduleCmd(*m_project, m_currentCalculationManager);
185         m_calculationCommand->redo();
186     }
187 }
188 
slotCalculationFinished(Project * p,ScheduleManager * sm)189 void MainDocument::slotCalculationFinished(Project *p, ScheduleManager *sm)
190 {
191     if (sm != m_currentCalculationManager) {
192         return;
193     }
194     delete m_calculationCommand;
195     m_calculationCommand = nullptr;
196     m_currentCalculationManager = m_nextCalculationManager;
197     m_nextCalculationManager = nullptr;
198     if (m_currentCalculationManager) {
199         QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
200     }
201 }
202 
setReadWrite(bool rw)203 void MainDocument::setReadWrite(bool rw)
204 {
205     m_config.setReadWrite(rw);
206     KoDocument::setReadWrite(rw);
207 }
208 
loadSchedulerPlugins()209 void MainDocument::loadSchedulerPlugins()
210 {
211     // Add built-in scheduler
212     addSchedulerPlugin("Built-in", new BuiltinSchedulerPlugin(this));
213 
214     // Add all real scheduler plugins
215     SchedulerPluginLoader *loader = new SchedulerPluginLoader(this);
216     connect(loader, &SchedulerPluginLoader::pluginLoaded, this, &MainDocument::addSchedulerPlugin);
217     loader->loadAllPlugins();
218 }
219 
addSchedulerPlugin(const QString & key,SchedulerPlugin * plugin)220 void MainDocument::addSchedulerPlugin(const QString &key, SchedulerPlugin *plugin)
221 {
222     debugPlan<<plugin;
223     m_schedulerPlugins[key] = plugin;
224 }
225 
configChanged()226 void MainDocument::configChanged()
227 {
228     //m_project->setConfig(m_config);
229 }
230 
setProject(Project * project)231 void MainDocument::setProject(Project *project)
232 {
233     if (m_project) {
234         delete m_project;
235     }
236     m_project = project;
237     if (m_project) {
238         connect(m_project, &Project::projectChanged, this, &MainDocument::changed);
239 //        m_project->setConfig(config());
240         m_project->setSchedulerPlugins(m_schedulerPlugins);
241 
242         // For auto scheduling
243         delete m_calculationCommand;
244         m_calculationCommand = nullptr;
245         m_currentCalculationManager = nullptr;
246         m_nextCalculationManager = nullptr;
247         connect(m_project, &Project::nodeAdded, this, &MainDocument::setCalculationNeeded);
248         connect(m_project, &Project::nodeRemoved, this, &MainDocument::setCalculationNeeded);
249         connect(m_project, &Project::relationAdded, this, &MainDocument::setCalculationNeeded);
250         connect(m_project, &Project::relationRemoved, this, &MainDocument::setCalculationNeeded);
251         connect(m_project, &Project::calendarChanged, this, &MainDocument::setCalculationNeeded);
252         connect(m_project, &Project::defaultCalendarChanged, this, &MainDocument::setCalculationNeeded);
253         connect(m_project, &Project::calendarAdded, this, &MainDocument::setCalculationNeeded);
254         connect(m_project, &Project::calendarRemoved, this, &MainDocument::setCalculationNeeded);
255         connect(m_project, &Project::scheduleManagerChanged, this, &MainDocument::slotScheduleManagerChanged);
256         connect(m_project, &Project::nodeChanged, this, &MainDocument::slotNodeChanged);
257         connect(m_project, &Project::sigCalculationFinished, this, &MainDocument::slotCalculationFinished);
258     }
259 
260     QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
261     if (!dir.isEmpty()) {
262         dir += "/taskmodules";
263         m_project->setLocalTaskModulesPath(QUrl::fromLocalFile(dir));
264     }
265     setTaskModulesWatch();
266     connect(project, &Project::taskModulesChanged, this, &MainDocument::setTaskModulesWatch);
267 
268     emit changed();
269 }
270 
setTaskModulesWatch()271 void MainDocument::setTaskModulesWatch()
272 {
273     delete m_taskModulesWatch;
274     m_taskModulesWatch = new KDirWatch(this);
275     for (const QUrl &url : m_project->taskModules()) {
276         m_taskModulesWatch->addDir(url.toLocalFile());
277     }
278     connect(m_taskModulesWatch, &KDirWatch::dirty, this, &MainDocument::taskModuleDirChanged);
279 }
280 
taskModuleDirChanged()281 void MainDocument::taskModuleDirChanged()
282 {
283     // HACK to trigger update FIXME
284     m_project->setUseLocalTaskModules(m_project->useLocalTaskModules());
285 }
286 
loadOdf(KoOdfReadStore & odfStore)287 bool MainDocument::loadOdf(KoOdfReadStore &odfStore)
288 {
289     warnPlan<< "OpenDocument not supported, let's try native xml format";
290     return loadXML(odfStore.contentDoc(), 0); // We have only one format, so try to load that!
291 }
292 
loadXML(const KoXmlDocument & document,KoStore *)293 bool MainDocument::loadXML(const KoXmlDocument &document, KoStore*)
294 {
295     QPointer<KoUpdater> updater;
296     if (progressUpdater()) {
297         updater = progressUpdater()->startSubtask(1, "Plan::Part::loadXML");
298         updater->setProgress(0);
299         m_xmlLoader.setUpdater(updater);
300     }
301 
302     QString value;
303     KoXmlElement plan = document.documentElement();
304 
305     // Check if this is the right app
306     value = plan.attribute("mime", QString());
307     if (value.isEmpty()) {
308         errorPlan << "No mime type specified!";
309         setErrorMessage(i18n("Invalid document. No mimetype specified."));
310         return false;
311     }
312     if (value == "application/x-vnd.kde.kplato") {
313         if (updater) {
314             updater->setProgress(5);
315         }
316         m_xmlLoader.setMimetype(value);
317         QString message;
318         Project *newProject = new Project(m_config, false);
319         KPlatoXmlLoader loader(m_xmlLoader, newProject);
320         bool ok = loader.load(plan);
321         if (ok) {
322             setProject(newProject);
323             setModified(false);
324             debugPlan<<newProject->schedules();
325             // Cleanup after possible bug:
326             // There should *not* be any deleted schedules (or with parent == 0)
327             foreach (Node *n, newProject->nodeDict()) {
328                 foreach (Schedule *s, n->schedules()) {
329                     if (s->isDeleted()) { // true also if parent == 0
330                         errorPlan<<n->name()<<s;
331                         n->takeSchedule(s);
332                         delete s;
333                     }
334                 }
335             }
336         } else {
337             setErrorMessage(loader.errorMessage());
338             delete newProject;
339         }
340         if (updater) {
341             updater->setProgress(100); // the rest is only processing, not loading
342         }
343         emit changed();
344         return ok;
345     }
346     if (value != "application/x-vnd.kde.plan") {
347         errorPlan << "Unknown mime type " << value;
348         setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan, got %1", value));
349         return false;
350     }
351     QString syntaxVersion = plan.attribute("version", PLAN_FILE_SYNTAX_VERSION);
352     m_xmlLoader.setVersion(syntaxVersion);
353     if (syntaxVersion > PLAN_FILE_SYNTAX_VERSION) {
354         KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
355                       0, i18n("This document was created with a newer version of Plan (syntax version: %1)\n"
356                                "Opening it in this version of Plan will lose some information.", syntaxVersion),
357                       i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
358         if (ret == KMessageBox::Cancel) {
359             setErrorMessage("USER_CANCELED");
360             return false;
361         }
362     }
363     if (updater) updater->setProgress(5);
364 /*
365 #ifdef KOXML_USE_QDOM
366     int numNodes = plan.childNodes().count();
367 #else
368     int numNodes = plan.childNodesCount();
369 #endif
370 */
371 #if 0
372 This test does not work any longer. KoXml adds a couple of elements not present in the file!!
373     if (numNodes > 2) {
374         //TODO: Make a proper bitching about this
375         debugPlan <<"*** Error ***";
376         debugPlan <<"  Children count should be maximum 2, but is" << numNodes;
377         return false;
378     }
379 #endif
380     m_xmlLoader.startLoad();
381     KoXmlNode n = plan.firstChild();
382     for (; ! n.isNull(); n = n.nextSibling()) {
383         if (! n.isElement()) {
384             continue;
385         }
386         KoXmlElement e = n.toElement();
387         if (e.tagName() == "project") {
388             Project *newProject = new Project(m_config, true);
389             m_xmlLoader.setProject(newProject);
390             if (newProject->load(e, m_xmlLoader)) {
391                 if (newProject->id().isEmpty()) {
392                     newProject->setId(newProject->uniqueNodeId());
393                     newProject->registerNodeId(newProject);
394                 }
395                 // The load went fine. Throw out the old project
396                 setProject(newProject);
397                 // Cleanup after possible bug:
398                 // There should *not* be any deleted schedules (or with parent == 0)
399                 foreach (Node *n, newProject->nodeDict()) {
400                     foreach (Schedule *s, n->schedules()) {
401                         if (s->isDeleted()) { // true also if parent == 0
402                             errorPlan<<n->name()<<s;
403                             n->takeSchedule(s);
404                             delete s;
405                         }
406                     }
407                 }
408             } else {
409                 delete newProject;
410                 m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of project failed");
411                 //TODO add some ui here
412             }
413         }
414     }
415     m_xmlLoader.stopLoad();
416 
417     if (updater) updater->setProgress(100); // the rest is only processing, not loading
418 
419     setModified(false);
420     emit changed();
421     return true;
422 }
423 
saveXML()424 QDomDocument MainDocument::saveXML()
425 {
426     debugPlan;
427     // Save the project
428     XmlSaveContext context(m_project);
429     context.save();
430 
431     return context.document;
432 }
433 
saveWorkPackageXML(const Node * node,long id,Resource * resource)434 QDomDocument MainDocument::saveWorkPackageXML(const Node *node, long id, Resource *resource)
435 {
436     debugPlanWp<<resource<<node;
437     QDomDocument document("plan");
438 
439     document.appendChild(document.createProcessingInstruction(
440                 "xml",
441     "version=\"1.0\" encoding=\"UTF-8\""));
442 
443     QDomElement doc = document.createElement("planwork");
444     doc.setAttribute("editor", "Plan");
445     doc.setAttribute("mime", "application/x-vnd.kde.plan.work");
446     doc.setAttribute("version", PLANWORK_FILE_SYNTAX_VERSION);
447     doc.setAttribute("plan-version", PLAN_FILE_SYNTAX_VERSION);
448     document.appendChild(doc);
449 
450     // Work package info
451     QDomElement wp = document.createElement("workpackage");
452     if (resource) {
453         wp.setAttribute("owner", resource->name());
454         wp.setAttribute("owner-id", resource->id());
455     }
456     wp.setAttribute("time-tag", QDateTime::currentDateTime().toString(Qt::ISODate));
457     wp.setAttribute("save-url", m_project->workPackageInfo().retrieveUrl.toString(QUrl::None));
458     wp.setAttribute("load-url", m_project->workPackageInfo().publishUrl.toString(QUrl::None));
459     debugPlanWp<<"publish:"<<m_project->workPackageInfo().publishUrl.toString(QUrl::None);
460     debugPlanWp<<"retrieve:"<<m_project->workPackageInfo().retrieveUrl.toString(QUrl::None);
461     doc.appendChild(wp);
462 
463     // Save the project
464     m_project->saveWorkPackageXML(doc, node, id);
465 
466     return document;
467 }
468 
saveWorkPackageToStream(QIODevice * dev,const Node * node,long id,Resource * resource)469 bool MainDocument::saveWorkPackageToStream(QIODevice *dev, const Node *node, long id, Resource *resource)
470 {
471     QDomDocument doc = saveWorkPackageXML(node, id, resource);
472     // Save to buffer
473     QByteArray s = doc.toByteArray(); // utf8 already
474     dev->open(QIODevice::WriteOnly);
475     int nwritten = dev->write(s.data(), s.size());
476     if (nwritten != (int)s.size()) {
477         warnPlanWp<<"wrote:"<<nwritten<<"- expected:"<< s.size();
478     }
479     return nwritten == (int)s.size();
480 }
481 
saveWorkPackageFormat(const QString & file,const Node * node,long id,Resource * resource)482 bool MainDocument::saveWorkPackageFormat(const QString &file, const Node *node, long id, Resource *resource)
483 {
484     debugPlanWp <<"Saving to store";
485 
486     KoStore::Backend backend = KoStore::Zip;
487 #ifdef QCA2
488 /*    if (d->m_specialOutputFlag == SaveEncrypted) {
489         backend = KoStore::Encrypted;
490         debugPlan <<"Saving using encrypted backend.";
491     }*/
492 #endif
493 
494     QByteArray mimeType = "application/x-vnd.kde.plan.work";
495     debugPlanWp <<"MimeType=" << mimeType;
496 
497     KoStore *store = KoStore::createStore(file, KoStore::Write, mimeType, backend);
498 /*    if (d->m_specialOutputFlag == SaveEncrypted && !d->m_password.isNull()) {
499         store->setPassword(d->m_password);
500     }*/
501     if (store->bad()) {
502         setErrorMessage(i18n("Could not create the workpackage file for saving: %1", file)); // more details needed?
503         delete store;
504         return false;
505     }
506     // Tell KoStore not to touch the file names
507 
508 
509     if (! store->open("root")) {
510         setErrorMessage(i18n("Not able to write '%1'. Partition full?", QString("maindoc.xml")));
511         delete store;
512         return false;
513     }
514     KoStoreDevice dev(store);
515     if (!saveWorkPackageToStream(&dev, node, id, resource) || !store->close()) {
516         errorPlanWp <<"saveToStream failed";
517         delete store;
518         return false;
519     }
520     node->documents().saveToStore(store);
521 
522     debugPlanWp <<"Saving done of url:" << file;
523     if (!store->finalize()) {
524         delete store;
525         return false;
526     }
527     // Success
528     delete store;
529 
530     return true;
531 }
532 
saveWorkPackageUrl(const QUrl & _url,const Node * node,long id,Resource * resource)533 bool MainDocument::saveWorkPackageUrl(const QUrl &_url, const Node *node, long id, Resource *resource)
534 {
535     debugPlanWp<<_url;
536     QApplication::setOverrideCursor(Qt::WaitCursor);
537     emit statusBarMessage(i18n("Saving..."));
538     bool ret = false;
539     ret = saveWorkPackageFormat(_url.path(), node, id, resource); // kzip don't handle file://
540     QApplication::restoreOverrideCursor();
541     emit clearStatusBarMessage();
542     return ret;
543 }
544 
loadWorkPackage(Project & project,const QUrl & url)545 bool MainDocument::loadWorkPackage(Project &project, const QUrl &url)
546 {
547     debugPlanWp<<url;
548     if (! url.isLocalFile()) {
549         warnPlanWp<<Q_FUNC_INFO<<"TODO: download if url not local";
550         return false;
551     }
552     KoStore *store = KoStore::createStore(url.path(), KoStore::Read, "", KoStore::Auto);
553     if (store->bad()) {
554 //        d->lastErrorMessage = i18n("Not a valid Calligra file: %1", file);
555         errorPlanWp<<"bad store"<<url.toDisplayString();
556         delete store;
557 //        QApplication::restoreOverrideCursor();
558         return false;
559     }
560     if (! store->open("root")) { // "old" file format (maindoc.xml)
561         // i18n("File does not have a maindoc.xml: %1", file);
562         errorPlanWp<<"No root"<<url.toDisplayString();
563         delete store;
564 //        QApplication::restoreOverrideCursor();
565         return false;
566     }
567     Package *package = 0;
568     KoXmlDocument doc;
569     QString errorMsg; // Error variables for QDomDocument::setContent
570     int errorLine, errorColumn;
571     bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
572     if (! ok) {
573         errorPlanWp << "Parsing error in " << url.url() << "! Aborting!" << endl
574                 << " In line: " << errorLine << ", column: " << errorColumn << endl
575                 << " Error message: " << errorMsg;
576         //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));
577     } else {
578         package = loadWorkPackageXML(project, store->device(), doc, url);
579         if (package) {
580             package->url = url;
581             m_workpackages.insert(package->timeTag, package);
582             if (!m_mergedPackages.contains(package->timeTag)) {
583                 m_mergedPackages[package->timeTag] = package->project; // register this for next time
584             }
585         } else {
586             ok = false;
587         }
588     }
589     store->close();
590     //###
591     if (ok && package && package->settings.documents) {
592         ok = extractFiles(store, package);
593     }
594     delete store;
595     if (! ok) {
596 //        QApplication::restoreOverrideCursor();
597         return false;
598     }
599     return true;
600 }
601 
loadWorkPackageXML(Project & project,QIODevice *,const KoXmlDocument & document,const QUrl & url)602 Package *MainDocument::loadWorkPackageXML(Project &project, QIODevice *, const KoXmlDocument &document, const QUrl &url)
603 {
604     QString value;
605     bool ok = true;
606     Project *proj = 0;
607     Package *package = 0;
608     KoXmlElement plan = document.documentElement();
609 
610     // Check if this is the right app
611     value = plan.attribute("mime", QString());
612     if (value.isEmpty()) {
613         errorPlanWp<<Q_FUNC_INFO<<"No mime type specified!";
614         setErrorMessage(i18n("Invalid document. No mimetype specified."));
615         return 0;
616     } else if (value == "application/x-vnd.kde.kplato.work") {
617         m_xmlLoader.setMimetype(value);
618         m_xmlLoader.setWorkVersion(plan.attribute("version", "0.0.0"));
619         proj = new Project();
620         KPlatoXmlLoader loader(m_xmlLoader, proj);
621         ok = loader.loadWorkpackage(plan);
622         if (! ok) {
623             setErrorMessage(loader.errorMessage());
624             delete proj;
625             return 0;
626         }
627         package = loader.package();
628         package->timeTag = QDateTime::fromString(loader.timeTag(), Qt::ISODate);
629     } else if (value != "application/x-vnd.kde.plan.work") {
630         errorPlanWp << "Unknown mime type " << value;
631         setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan.work, got %1", value));
632         return 0;
633     } else {
634         if (plan.attribute("editor") != QStringLiteral("PlanWork")) {
635             warnPlanWp<<"Skipped work package file not generated with PlanWork:"<<plan.attribute("editor")<<url;
636             return nullptr;
637         }
638         QString syntaxVersion = plan.attribute("version", "0.0.0");
639         m_xmlLoader.setWorkVersion(syntaxVersion);
640         if (syntaxVersion > PLANWORK_FILE_SYNTAX_VERSION) {
641             KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
642                     0, i18n("This document was created with a newer version of PlanWork (syntax version: %1)\n"
643                     "Opening it in this version of PlanWork will lose some information.", syntaxVersion),
644                     i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
645             if (ret == KMessageBox::Cancel) {
646                 setErrorMessage("USER_CANCELED");
647                 return 0;
648             }
649         }
650         m_xmlLoader.setVersion(plan.attribute("plan-version", PLAN_FILE_SYNTAX_VERSION));
651         m_xmlLoader.startLoad();
652         proj = new Project();
653         package = new Package();
654         package->project = proj;
655         KoXmlNode n = plan.firstChild();
656         for (; ! n.isNull(); n = n.nextSibling()) {
657             if (! n.isElement()) {
658                 continue;
659             }
660             KoXmlElement e = n.toElement();
661             if (e.tagName() == "project") {
662                 m_xmlLoader.setProject(proj);
663                 ok = proj->load(e, m_xmlLoader);
664                 if (! ok) {
665                     m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of work package failed");
666                     warnPlanWp<<"Skip workpackage:"<<"Loading project failed";
667                     //TODO add some ui here
668                 }
669             } else if (e.tagName() == "workpackage") {
670                 package->timeTag = QDateTime::fromString(e.attribute("time-tag"), Qt::ISODate);
671                 package->ownerId = e.attribute("owner-id");
672                 package->ownerName = e.attribute("owner");
673                 debugPlan<<"workpackage:"<<package->timeTag<<package->ownerId<<package->ownerName;
674                 KoXmlElement elem;
675                 forEachElement(elem, e) {
676                     if (elem.tagName() != "settings") {
677                         continue;
678                     }
679                     package->settings.usedEffort = (bool)elem.attribute("used-effort").toInt();
680                     package->settings.progress = (bool)elem.attribute("progress").toInt();
681                     package->settings.documents = (bool)elem.attribute("documents").toInt();
682                 }
683             }
684         }
685         if (proj->numChildren() > 0) {
686             package->task = static_cast<Task*>(proj->childNode(0));
687             package->toTask = qobject_cast<Task*>(m_project->findNode(package->task->id()));
688             WorkPackage &wp = package->task->workPackage();
689             if (wp.ownerId().isEmpty()) {
690                 wp.setOwnerId(package->ownerId);
691                 wp.setOwnerName(package->ownerName);
692             }
693             if (wp.ownerId() != package->ownerId) {
694                 warnPlanWp<<"Current owner:"<<wp.ownerName()<<"not the same as package owner:"<<package->ownerName;
695             }
696             debugPlanWp<<"Task set:"<<package->task->name();
697         }
698         m_xmlLoader.stopLoad();
699     }
700     if (ok && proj->id() != project.id()) {
701         debugPlanWp<<"Skip workpackage:"<<"Not the correct project";
702         ok = false;
703     }
704     if (ok && (package->task == nullptr)) {
705         warnPlanWp<<"Skip workpackage:"<<"No task in workpackage file";
706         ok = false;
707     }
708     if (ok && (package->toTask == nullptr)) {
709         warnPlanWp<<"Skip workpackage:"<<"Cannot find task:"<<package->task->id()<<package->task->name();
710         ok = false;
711     }
712     if (ok && !package->timeTag.isValid()) {
713         warnPlanWp<<"Work package is not time tagged:"<<package->task->name()<<package->url;
714         ok = false;
715     }
716     if (ok && m_mergedPackages.contains(package->timeTag)) {
717         debugPlanWp<<"Skip workpackage:"<<"already merged:"<<package->task->name()<<package->url;
718         ok = false; // already merged
719     }
720     if (!ok) {
721         delete proj;
722         delete package;
723         return nullptr;
724     }
725     return package;
726 }
727 
extractFiles(KoStore * store,Package * package)728 bool MainDocument::extractFiles(KoStore *store, Package *package)
729 {
730     if (package->task == 0) {
731         errorPlan<<"No task!";
732         return false;
733     }
734     foreach (Document *doc, package->task->documents().documents()) {
735         if (! doc->isValid() || doc->type() != Document::Type_Product || doc->sendAs() != Document::SendAs_Copy) {
736             continue;
737         }
738         if (! extractFile(store, package, doc)) {
739             return false;
740         }
741     }
742     return true;
743 }
744 
extractFile(KoStore * store,Package * package,const Document * doc)745 bool MainDocument::extractFile(KoStore *store, Package *package, const Document *doc)
746 {
747     QTemporaryFile tmpfile;
748     if (! tmpfile.open()) {
749         errorPlan<<"Failed to open temporary file";
750         return false;
751     }
752     if (! store->extractFile(doc->url().fileName(), tmpfile.fileName())) {
753         errorPlan<<"Failed to extract file:"<<doc->url().fileName()<<"to:"<<tmpfile.fileName();
754         return false;
755     }
756     package->documents.insert(tmpfile.fileName(), doc->url());
757     tmpfile.setAutoRemove(false);
758     debugPlan<<"extracted:"<<doc->url().fileName()<<"->"<<tmpfile.fileName();
759     return true;
760 }
761 
autoCheckForWorkPackages()762 void MainDocument::autoCheckForWorkPackages()
763 {
764     QTimer *timer = qobject_cast<QTimer*>(sender());
765     if (m_project && m_project->workPackageInfo().checkForWorkPackages) {
766         checkForWorkPackages(true);
767     }
768     if (timer && timer->interval() != 10000) {
769         timer->stop();
770         timer->setInterval(10000);
771         timer->start();
772     }
773 }
774 
checkForWorkPackages(bool keep)775 void MainDocument::checkForWorkPackages(bool keep)
776 {
777     if (m_checkingForWorkPackages || m_project == nullptr || m_project->numChildren() == 0 || m_project->workPackageInfo().retrieveUrl.isEmpty()) {
778         return;
779     }
780     if (! keep) {
781         qDeleteAll(m_mergedPackages);
782         m_mergedPackages.clear();
783     }
784     QDir dir(m_project->workPackageInfo().retrieveUrl.path(), "*.planwork");
785     m_infoList = dir.entryInfoList(QDir::Files | QDir::Readable, QDir::Time);
786     checkForWorkPackage();
787     return;
788 }
789 
checkForWorkPackage()790 void MainDocument::checkForWorkPackage()
791 {
792     if (! m_infoList.isEmpty()) {
793         m_checkingForWorkPackages = true;
794         QUrl url = QUrl::fromLocalFile(m_infoList.takeLast().absoluteFilePath());
795         if (!m_skipUrls.contains(url) && !loadWorkPackage(*m_project, url)) {
796             m_skipUrls << url;
797             debugPlanWp<<"skip url:"<<url;
798         }
799         if (! m_infoList.isEmpty()) {
800             QTimer::singleShot (0, this, &MainDocument::checkForWorkPackage);
801             return;
802         }
803         // Merge our workpackages
804         if (! m_workpackages.isEmpty()) {
805             emit workPackageLoaded();
806         }
807         m_checkingForWorkPackages = false;
808     }
809 }
810 
terminateWorkPackage(const Package * package)811 void MainDocument::terminateWorkPackage(const Package *package)
812 {
813     debugPlanWp<<package->toTask<<package->url;
814     if (m_workpackages.value(package->timeTag) == package) {
815         m_workpackages.remove(package->timeTag);
816     }
817     QFile file(package->url.path());
818     if (! file.exists()) {
819         warnPlanWp<<"File does not exist:"<<package->toTask<<package->url;
820         return;
821     }
822     Project::WorkPackageInfo wpi = m_project->workPackageInfo();
823     debugPlanWp<<"retrieve:"<<wpi.retrieveUrl<<"archive:"<<wpi.archiveUrl;
824     bool rename = wpi.retrieveUrl == package->url.adjusted(QUrl::RemoveFilename);
825     if (wpi.archiveAfterRetrieval && wpi.archiveUrl.isValid()) {
826         QDir dir(wpi.archiveUrl.path());
827         if (! dir.exists()) {
828             if (! dir.mkpath(dir.path())) {
829                 //TODO message
830                 warnPlanWp<<"Failed to create archive directory:"<<dir.path();
831                 return;
832             }
833         }
834         QFileInfo from(file);
835         QString name = dir.absolutePath() + '/' + from.fileName();
836         debugPlanWp<<"rename:"<<rename;
837         if (rename ? !file.rename(name) : !file.copy(name)) {
838             // try to create a unique name in case name already existed
839             debugPlanWp<<"Archive exists, create unique file name";
840             name = dir.absolutePath() + '/';
841             name += from.completeBaseName() + "-%1";
842             if (! from.suffix().isEmpty()) {
843                 name += '.' + from.suffix();
844             }
845             int i = 0;
846             bool ok = false;
847             while (! ok && i < 1000) {
848                 ++i;
849                 ok = rename ? QFile::rename(file.fileName(), name.arg(i)) : QFile::copy(file.fileName(), name.arg(i));
850             }
851             if (! ok) {
852                 //TODO message
853                 warnPlanWp<<"terminateWorkPackage: Failed to save"<<file.fileName();
854             }
855         }
856     } else if (wpi.deleteAfterRetrieval) {
857         if (rename) {
858             debugPlanWp<<"removed package file:"<<file.fileName();
859             file.remove();
860         } else {
861             debugPlanWp<<"package file not in 'from' dir:"<<file.fileName();
862         }
863     } else {
864         warnPlanWp<<"Cannot terminate package, archive:"<<wpi.archiveUrl;
865     }
866 }
867 
paintContent(QPainter &,const QRect &)868 void MainDocument::paintContent(QPainter &, const QRect &)
869 {
870     // Don't embed this app!!!
871 }
872 
slotViewDestroyed()873 void MainDocument::slotViewDestroyed()
874 {
875 }
876 
setLoadingTemplate(bool loading)877 void MainDocument::setLoadingTemplate(bool loading)
878 {
879     m_loadingTemplate = loading;
880 }
881 
setLoadingSharedResourcesTemplate(bool loading)882 void MainDocument::setLoadingSharedResourcesTemplate(bool loading)
883 {
884     m_loadingSharedResourcesTemplate = loading;
885 }
886 
completeLoading(KoStore * store)887 bool MainDocument::completeLoading(KoStore *store)
888 {
889     // If we get here the new project is loaded and set
890     if (m_loadingSharedProject) {
891         // this file is loaded by another project
892         // to read resource appointments,
893         // so we must not load any extra stuff
894         return true;
895     }
896     if (m_loadingTemplate) {
897         //debugPlan<<"Loading template, generate unique ids";
898         m_project->generateUniqueIds();
899         m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime));
900         m_project->setConstraintEndTime(m_project->constraintStartTime().addYears(2));
901         m_project->locale()->setCurrencyLocale(QLocale::AnyLanguage, QLocale::AnyCountry);
902         m_project->locale()->setCurrencySymbol(QString());
903     } else if (isImporting()) {
904         // NOTE: I don't think this is a good idea.
905         // Let the filter generate ids for non-plan files.
906         // If the user wants to create a new project from an old one,
907         // he should use Tools -> Insert Project File
908 
909         //m_project->generateUniqueNodeIds();
910     }
911     if (m_loadingSharedResourcesTemplate && m_project->calendarCount() > 0) {
912         Calendar *c = m_project->calendarAt(0);
913         c->setTimeZone(QTimeZone::systemTimeZone());
914     }
915     if (m_project->useSharedResources() && !m_project->sharedResourcesFile().isEmpty() && !m_skipSharedProjects) {
916         QUrl url = QUrl::fromLocalFile(m_project->sharedResourcesFile());
917         if (url.isValid()) {
918             insertResourcesFile(url, m_project->loadProjectsAtStartup() ? m_project->sharedProjectsUrl() : QUrl());
919         }
920     }
921     if (store == 0) {
922         // can happen if loading a template
923         debugPlan<<"No store";
924         return true; // continue anyway
925     }
926     delete m_context;
927     m_context = new Context();
928     KoXmlDocument doc;
929     if (loadAndParse(store, "context.xml", doc)) {
930         store->close();
931         m_context->load(doc);
932     } else warnPlan<<"No context";
933     return true;
934 }
935 
936 // TODO:
937 // Due to splitting of KoDocument into a document and a part,
938 // we simulate the old behaviour by registering all views in the document.
939 // Find a better solution!
registerView(View * view)940 void MainDocument::registerView(View* view)
941 {
942     if (view && ! m_views.contains(view)) {
943         m_views << QPointer<View>(view);
944     }
945 }
946 
completeSaving(KoStore * store)947 bool MainDocument::completeSaving(KoStore *store)
948 {
949     if (m_context && m_views.isEmpty()) {
950         if (store->open("context.xml")) {
951             // When e.g. saving as a template there are no views,
952             // so we cannot get info from them.
953             // Just use the context info we have in this case.
954             KoStoreDevice dev(store);
955             QByteArray s = m_context->document().toByteArray();
956             (void)dev.write(s.data(), s.size());
957             (void)store->close();
958             return true;
959         }
960         return false;
961     }
962     foreach (View *view, m_views) {
963         if (view) {
964             if (store->open("context.xml")) {
965                 if (m_context == 0) m_context = new Context();
966                 QDomDocument doc = m_context->save(view);
967 
968                 KoStoreDevice dev(store);
969                 QByteArray s = doc.toByteArray(); // this is already Utf8!
970                 (void)dev.write(s.data(), s.size());
971                 (void)store->close();
972 
973                 m_viewlistModified = false;
974                 emit viewlistModified(false);
975             }
976             break;
977         }
978     }
979     return true;
980 }
981 
loadAndParse(KoStore * store,const QString & filename,KoXmlDocument & doc)982 bool MainDocument::loadAndParse(KoStore *store, const QString &filename, KoXmlDocument &doc)
983 {
984     //debugPlan << "oldLoadAndParse: Trying to open " << filename;
985 
986     if (!store->open(filename))
987     {
988         warnPlan << "Entry " << filename << " not found!";
989 //        d->lastErrorMessage = i18n("Could not find %1",filename);
990         return false;
991     }
992     // Error variables for QDomDocument::setContent
993     QString errorMsg;
994     int errorLine, errorColumn;
995     bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
996     if (!ok)
997     {
998         errorPlan << "Parsing error in " << filename << "! Aborting!" << endl
999             << " In line: " << errorLine << ", column: " << errorColumn << endl
1000             << " Error message: " << errorMsg;
1001 /*        d->lastErrorMessage = i18n("Parsing error in %1 at line %2, column %3\nError message: %4"
1002                               ,filename  ,errorLine, errorColumn ,
1003                               QCoreApplication::translate("QXml", errorMsg.toUtf8(), 0,
1004                                   QCoreApplication::UnicodeUTF8));*/
1005         store->close();
1006         return false;
1007     }
1008     debugPlan << "File " << filename << " loaded and parsed";
1009     return true;
1010 }
1011 
insertFile(const QUrl & url,Node * parent,Node * after)1012 void MainDocument::insertFile(const QUrl &url, Node *parent, Node *after)
1013 {
1014     Part *part = new Part(this);
1015     MainDocument *doc = new MainDocument(part);
1016     part->setDocument(doc);
1017     doc->disconnect(); // doc shall not handle feedback from openUrl()
1018     doc->setAutoSave(0); //disable
1019     doc->m_insertFileInfo.url = url;
1020     doc->m_insertFileInfo.parent = parent;
1021     doc->m_insertFileInfo.after = after;
1022     connect(doc, &KoDocument::completed, this, &MainDocument::insertFileCompleted);
1023     connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled);
1024 
1025     m_isLoading = true;
1026     doc->openUrl(url);
1027 }
1028 
insertFileCompleted()1029 void MainDocument::insertFileCompleted()
1030 {
1031     debugPlan<<sender();
1032     MainDocument *doc = qobject_cast<MainDocument*>(sender());
1033     if (doc) {
1034         Project &p = doc->getProject();
1035         insertProject(p, doc->m_insertFileInfo.parent, doc->m_insertFileInfo.after);
1036         doc->documentPart()->deleteLater(); // also deletes document
1037     } else {
1038         KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1039     }
1040     m_isLoading = false;
1041 }
1042 
insertResourcesFile(const QUrl & url,const QUrl & projects)1043 void MainDocument::insertResourcesFile(const QUrl &url, const QUrl &projects)
1044 {
1045     insertSharedProjects(projects); // prepare for insertion after shared resources
1046     m_sharedProjectsFiles.removeAll(url); // resource file is not a project
1047 
1048     Part *part = new Part(this);
1049     MainDocument *doc = new MainDocument(part);
1050     doc->m_skipSharedProjects = true; // should not have shared projects, but...
1051     part->setDocument(doc);
1052     doc->disconnect(); // doc shall not handle feedback from openUrl()
1053     doc->setAutoSave(0); //disable
1054     doc->setCheckAutoSaveFile(false);
1055     connect(doc, &KoDocument::completed, this, &MainDocument::insertResourcesFileCompleted);
1056     connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled);
1057 
1058     m_isLoading = true;
1059     doc->openUrl(url);
1060 
1061 }
1062 
insertResourcesFileCompleted()1063 void MainDocument::insertResourcesFileCompleted()
1064 {
1065     debugPlanShared<<sender();
1066     MainDocument *doc = qobject_cast<MainDocument*>(sender());
1067     if (doc) {
1068         Project &p = doc->getProject();
1069         mergeResources(p);
1070         m_project->setSharedResourcesLoaded(true);
1071         doc->documentPart()->deleteLater(); // also deletes document
1072         slotInsertSharedProject(); // insert shared bookings
1073     } else {
1074         KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1075     }
1076     m_isLoading = false;
1077 }
1078 
insertFileCancelled(const QString & error)1079 void MainDocument::insertFileCancelled(const QString &error)
1080 {
1081     debugPlan<<sender()<<"error="<<error;
1082     if (! error.isEmpty()) {
1083         KMessageBox::error(0, error);
1084     }
1085     MainDocument *doc = qobject_cast<MainDocument*>(sender());
1086     if (doc) {
1087         doc->documentPart()->deleteLater(); // also deletes document
1088     }
1089     m_isLoading = false;
1090 }
1091 
clearResourceAssignments()1092 void MainDocument::clearResourceAssignments()
1093 {
1094     foreach (Resource *r, m_project->resourceList()) {
1095         r->clearExternalAppointments();
1096     }
1097 }
1098 
loadResourceAssignments(QUrl url)1099 void MainDocument::loadResourceAssignments(QUrl url)
1100 {
1101     insertSharedProjects(url);
1102     slotInsertSharedProject();
1103 }
1104 
insertSharedProjects(const QList<QUrl> & urls)1105 void MainDocument::insertSharedProjects(const QList<QUrl> &urls)
1106 {
1107     clearResourceAssignments();
1108     m_sharedProjectsFiles = urls;
1109     slotInsertSharedProject();
1110 }
1111 
insertSharedProjects(const QUrl & url)1112 void MainDocument::insertSharedProjects(const QUrl &url)
1113 {
1114     m_sharedProjectsFiles.clear();
1115     QFileInfo fi(url.path());
1116     if (!fi.exists()) {
1117         return;
1118     }
1119     if (fi.isFile()) {
1120         m_sharedProjectsFiles = QList<QUrl>() << url;
1121         debugPlan<<"Get all projects in file:"<<url;
1122     } else if (fi.isDir()) {
1123         // Get all plan files in this directory
1124         debugPlan<<"Get all projects in dir:"<<url;
1125         QDir dir = fi.dir();
1126         foreach(const QString &f, dir.entryList(QStringList()<<"*.plan")) {
1127             QString path = dir.canonicalPath();
1128             if (path.isEmpty()) {
1129                 continue;
1130             }
1131             path += '/' + f;
1132             QUrl u(path);
1133             u.setScheme("file");
1134             m_sharedProjectsFiles << u;
1135         }
1136     } else {
1137         warnPlan<<"Unknown url:"<<url<<url.path()<<url.fileName();
1138         return;
1139     }
1140     clearResourceAssignments();
1141 }
1142 
slotInsertSharedProject()1143 void MainDocument::slotInsertSharedProject()
1144 {
1145     debugPlan<<m_sharedProjectsFiles;
1146     if (m_sharedProjectsFiles.isEmpty()) {
1147         return;
1148     }
1149     Part *part = new Part(this);
1150     MainDocument *doc = new MainDocument(part);
1151     doc->m_skipSharedProjects = true; // never load recursively
1152     part->setDocument(doc);
1153     doc->disconnect(); // doc shall not handle feedback from openUrl()
1154     doc->setAutoSave(0); //disable
1155     doc->setCheckAutoSaveFile(false);
1156     doc->m_loadingSharedProject = true;
1157     connect(doc, &KoDocument::completed, this, &MainDocument::insertSharedProjectCompleted);
1158     connect(doc, &KoDocument::canceled, this, &MainDocument::insertSharedProjectCancelled);
1159 
1160     m_isLoading = true;
1161     doc->openUrl(m_sharedProjectsFiles.takeFirst());
1162 }
1163 
insertSharedProjectCompleted()1164 void MainDocument::insertSharedProjectCompleted()
1165 {
1166     debugPlanShared<<sender();
1167     MainDocument *doc = qobject_cast<MainDocument*>(sender());
1168     if (doc) {
1169         Project &p = doc->getProject();
1170         debugPlanShared<<m_project->id()<<"Loaded project:"<<p.id()<<p.name();
1171         if (p.id() != m_project->id() && p.isScheduled(ANYSCHEDULED)) {
1172             // FIXME: improve!
1173             // find a suitable schedule
1174             ScheduleManager *sm = 0;
1175             foreach(ScheduleManager *m, p.allScheduleManagers()) {
1176                 if (m->isBaselined()) {
1177                     sm = m;
1178                     break;
1179                 }
1180                 if (m->isScheduled()) {
1181                     sm = m; // take the last one, more likely to be subschedule
1182                 }
1183             }
1184             if (sm) {
1185                 foreach(Resource *r, p.resourceList()) {
1186                     Resource *res = m_project->resource(r->id());
1187                     if (res && res->isShared()) {
1188                         Appointment *app = new Appointment();
1189                         app->setAuxcilliaryInfo(p.name());
1190                         foreach(const Appointment *a, r->appointments(sm->scheduleId())) {
1191                             *app += *a;
1192                         }
1193                         if (app->isEmpty()) {
1194                             delete app;
1195                         } else {
1196                             res->addExternalAppointment(p.id(), app);
1197                             debugPlanShared<<res->name()<<"added:"<<app->auxcilliaryInfo()<<app;
1198                         }
1199                     }
1200                 }
1201             }
1202         }
1203         doc->documentPart()->deleteLater(); // also deletes document
1204         m_isLoading = false;
1205         emit insertSharedProject(); // do next file
1206     } else {
1207         KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1208         m_isLoading = false;
1209     }
1210 }
1211 
insertSharedProjectCancelled(const QString & error)1212 void MainDocument::insertSharedProjectCancelled(const QString &error)
1213 {
1214     debugPlanShared<<sender()<<"error="<<error;
1215     if (! error.isEmpty()) {
1216         KMessageBox::error(0, error);
1217     }
1218     MainDocument *doc = qobject_cast<MainDocument*>(sender());
1219     if (doc) {
1220         doc->documentPart()->deleteLater(); // also deletes document
1221     }
1222     m_isLoading = false;
1223 }
1224 
insertProject(Project & project,Node * parent,Node * after)1225 bool MainDocument::insertProject(Project &project, Node *parent, Node *after)
1226 {
1227     debugPlan<<&project;
1228     // make sure node ids in new project is unique also in old project
1229     QList<QString> existingIds = m_project->nodeDict().keys();
1230     foreach (Node *n, project.allNodes()) {
1231         QString oldid = n->id();
1232         n->setId(project.uniqueNodeId(existingIds));
1233         project.removeId(oldid); // remove old id
1234         project.registerNodeId(n); // register new id
1235     }
1236     MacroCommand *m = new InsertProjectCmd(project, parent==0?m_project:parent, after, kundo2_i18n("Insert project"));
1237     if (m->isEmpty()) {
1238         delete m;
1239     } else {
1240         addCommand(m);
1241     }
1242     return true;
1243 }
1244 
1245 // check if calendar 'c' has children that will not be removed (normally 'Local' calendars)
canRemoveCalendar(const Calendar * c,const QList<Calendar * > & lst)1246 bool canRemoveCalendar(const Calendar *c, const QList<Calendar*> &lst)
1247 {
1248     for (Calendar *cc : c->calendars()) {
1249         if (!lst.contains(cc)) {
1250             return false;
1251         }
1252         if (!canRemoveCalendar(cc, lst)) {
1253             return false;
1254         }
1255     }
1256     return true;
1257 }
1258 
1259 // sort parent calendars before children
sortedRemoveCalendars(Project & shared,const QList<Calendar * > & lst)1260 QList<Calendar*> sortedRemoveCalendars(Project &shared, const QList<Calendar*> &lst) {
1261     QList<Calendar*> result;
1262     for (Calendar *c : lst) {
1263         if (c->isShared() && !shared.calendar(c->id())) {
1264             result << c;
1265         }
1266         result += sortedRemoveCalendars(shared, c->calendars());
1267     }
1268     return result;
1269 }
1270 
mergeResources(Project & project)1271 bool MainDocument::mergeResources(Project &project)
1272 {
1273     debugPlanShared<<&project;
1274     // Just in case, remove stuff not related to resources
1275     foreach(Node *n,  project.childNodeIterator()) {
1276         debugPlanShared<<"Project not empty, delete node:"<<n<<n->name();
1277         NodeDeleteCmd cmd(n);
1278         cmd.execute();
1279     }
1280     foreach(ScheduleManager *m,  project.scheduleManagers()) {
1281         debugPlanShared<<"Project not empty, delete schedule:"<<m<<m->name();
1282         DeleteScheduleManagerCmd cmd(project, m);
1283         cmd.execute();
1284     }
1285     foreach(Account *a, project.accounts().accountList()) {
1286         debugPlanShared<<"Project not empty, delete account:"<<a<<a->name();
1287         RemoveAccountCmd cmd(project, a);
1288         cmd.execute();
1289     }
1290     // Mark all resources / groups as shared
1291     foreach(ResourceGroup *g, project.resourceGroups()) {
1292         g->setShared(true);
1293     }
1294     foreach(Resource *r, project.resourceList()) {
1295         r->setShared(true);
1296     }
1297     // Mark all calendars shared
1298     foreach(Calendar *c, project.allCalendars()) {
1299         c->setShared(true);
1300     }
1301     // check if any shared stuff has been removed
1302     QList<ResourceGroup*> removedGroups;
1303     QList<Resource*> removedResources;
1304     QList<Calendar*> removedCalendars;
1305     QStringList removed;
1306     foreach(ResourceGroup *g, m_project->resourceGroups()) {
1307         if (g->isShared() && !project.findResourceGroup(g->id())) {
1308             removedGroups << g;
1309             removed << i18n("Group: %1", g->name());
1310         }
1311     }
1312     foreach(Resource *r, m_project->resourceList()) {
1313         if (r->isShared() && !project.findResource(r->id())) {
1314             removedResources << r;
1315             removed << i18n("Resource: %1", r->name());
1316         }
1317     }
1318     removedCalendars = sortedRemoveCalendars(project, m_project->calendars());
1319     for (Calendar *c : qAsConst(removedCalendars)) {
1320         removed << i18n("Calendar: %1", c->name());
1321     }
1322     if (!removed.isEmpty()) {
1323         KMessageBox::ButtonCode result = KMessageBox::warningYesNoCancelList(
1324                     0,
1325                     i18n("Shared resources has been removed from the shared resources file."
1326                          "\nSelect how they shall be treated in this project."),
1327                     removed,
1328                     xi18nc("@title:window", "Shared resources"),
1329                     KStandardGuiItem::remove(),
1330                     KGuiItem(i18n("Convert")),
1331                     KGuiItem(i18n("Keep"))
1332                     );
1333         switch (result) {
1334         case KMessageBox::Yes: // Remove
1335             for (Resource *r : qAsConst(removedResources)) {
1336                 RemoveResourceCmd cmd(r->parentGroup(), r);
1337                 cmd.redo();
1338             }
1339             for (ResourceGroup *g : qAsConst(removedGroups)) {
1340                 if (g->resources().isEmpty()) {
1341                     RemoveResourceGroupCmd cmd(m_project, g);
1342                     cmd.redo();
1343                 } else {
1344                     // we may have put local resource(s) in this group
1345                     // so we need to keep it
1346                     g->setShared(false);
1347                     m_project->removeResourceGroupId(g->id());
1348                     g->setId(m_project->uniqueResourceGroupId());
1349                     m_project->insertResourceGroupId(g->id(), g);
1350                 }
1351             }
1352             for (Calendar *c : qAsConst(removedCalendars)) {
1353                 CalendarRemoveCmd cmd(m_project, c);
1354                 cmd.redo();
1355             }
1356             break;
1357         case KMessageBox::No: // Convert
1358             for (Resource *r : qAsConst(removedResources)) {
1359                 r->setShared(false);
1360                 m_project->removeResourceId(r->id());
1361                 r->setId(m_project->uniqueResourceId());
1362                 m_project->insertResourceId(r->id(), r);
1363             }
1364             for (ResourceGroup *g : qAsConst(removedGroups)) {
1365                 g->setShared(false);
1366                 m_project->removeResourceGroupId(g->id());
1367                 g->setId(m_project->uniqueResourceGroupId());
1368                 m_project->insertResourceGroupId(g->id(), g);
1369             }
1370             for (Calendar *c : qAsConst(removedCalendars)) {
1371                 c->setShared(false);
1372                 m_project->removeCalendarId(c->id());
1373                 c->setId(m_project->uniqueCalendarId());
1374                 m_project->insertCalendarId(c->id(), c);
1375             }
1376             break;
1377         case KMessageBox::Cancel: // Keep
1378             break;
1379         default:
1380             break;
1381         }
1382     }
1383     // update values of already existing objects
1384     QStringList l1;
1385     foreach(ResourceGroup *g, project.resourceGroups()) {
1386         l1 << g->id();
1387     }
1388     QStringList l2;
1389     foreach(ResourceGroup *g, m_project->resourceGroups()) {
1390         l2 << g->id();
1391     }
1392     debugPlanShared<<endl<<"  This:"<<l2<<endl<<"Shared:"<<l1;
1393     QList<ResourceGroup*> removegroups;
1394     foreach(ResourceGroup *g, project.resourceGroups()) {
1395         ResourceGroup *group = m_project->findResourceGroup(g->id());
1396         if (group) {
1397             if (!group->isShared()) {
1398                 // User has probably created shared resources from this project,
1399                 // so the resources exists but are local ones.
1400                 // Convert to shared and do not load the group from shared.
1401                 removegroups << g;
1402                 group->setShared(true);
1403                 debugPlanShared<<"Set group to shared:"<<group<<group->id();
1404             }
1405             group->setName(g->name());
1406             group->setType(g->type());
1407             debugPlanShared<<"Updated group:"<<group<<group->id();
1408         }
1409     }
1410     QList<Resource*> removeresources;
1411     foreach(Resource *r, project.resourceList()) {
1412         Resource *resource = m_project->findResource(r->id());
1413         if (resource) {
1414             if (!resource->isShared()) {
1415                 // User has probably created shared resources from this project,
1416                 // so the resources exists but are local ones.
1417                 // Convert to shared and do not load the resource from shared.
1418                 removeresources << r;
1419                 resource->setShared(true);
1420                 debugPlanShared<<"Set resource to shared:"<<resource<<resource->id();
1421             }
1422             resource->setName(r->name());
1423             resource->setInitials(r->initials());
1424             resource->setEmail(r->email());
1425             resource->setType(r->type());
1426             resource->setAutoAllocate(r->autoAllocate());
1427             resource->setAvailableFrom(r->availableFrom());
1428             resource->setAvailableUntil(r->availableUntil());
1429             resource->setUnits(r->units());
1430             resource->setNormalRate(r->normalRate());
1431             resource->setOvertimeRate(r->overtimeRate());
1432 
1433             QString id = r->calendar(true) ? r->calendar(true)->id() : QString();
1434             resource->setCalendar(m_project->findCalendar(id));
1435 
1436             id = r->account() ? r->account()->name() : QString();
1437             resource->setAccount(m_project->accounts().findAccount(id));
1438 
1439             resource->setRequiredIds(r->requiredIds());
1440 
1441             resource->setTeamMemberIds(r->teamMemberIds());
1442             debugPlanShared<<"Updated resource:"<<resource<<resource->id();
1443         }
1444     }
1445     QList<Calendar*> removecalendars;
1446     foreach(Calendar *c, project.allCalendars()) {
1447         Calendar *calendar = m_project->findCalendar(c->id());
1448         if (calendar) {
1449             if (!calendar->isShared()) {
1450                 // User has probably created shared resources from this project,
1451                 // so the calendar exists but are local ones.
1452                 // Convert to shared and do not load the resource from shared.
1453                 removecalendars << c;
1454                 calendar->setShared(true);
1455                 debugPlanShared<<"Set calendar to shared:"<<calendar<<calendar->id();
1456             }
1457             *calendar = *c;
1458             debugPlanShared<<"Updated calendar:"<<calendar<<calendar->id();
1459         }
1460     }
1461     debugPlanShared<<"Remove:"<<endl<<"calendars:"<<removecalendars<<endl<<"resources:"<<removeresources<<endl<<"groups:"<<removegroups;
1462     while (!removecalendars.isEmpty()) {
1463         for (int i = 0; i < removecalendars.count(); ++i) {
1464             Calendar *c = removecalendars.at(i);
1465             if (c->childCount() == 0) {
1466                 removecalendars.removeAt(i);
1467                 debugPlanShared<<"Delete calendar:"<<c<<c->id();
1468                 CalendarRemoveCmd cmd(&project, c);
1469                 cmd.execute();
1470             }
1471         }
1472     }
1473     for (Resource *r : qAsConst(removeresources)) {
1474         debugPlanShared<<"Delete resource:"<<r<<r->id();
1475         RemoveResourceCmd cmd(r->parentGroup(), r);
1476         cmd.execute();
1477     }
1478     for (ResourceGroup *g : qAsConst(removegroups)) {
1479         debugPlanShared<<"Delete group:"<<g<<g->id();
1480         RemoveResourceGroupCmd cmd(&project, g);
1481         cmd.execute();
1482     }
1483     // insert new objects
1484     Q_ASSERT(project.childNodeIterator().isEmpty());
1485     InsertProjectCmd cmd(project, m_project, 0);
1486     cmd.execute();
1487     return true;
1488 }
1489 
insertViewListItem(View *,const ViewListItem * item,const ViewListItem * parent,int index)1490 void MainDocument::insertViewListItem(View */*view*/, const ViewListItem *item, const ViewListItem *parent, int index)
1491 {
1492     // FIXME callers should take care that they now get a signal even if originating from themselves
1493     emit viewListItemAdded(item, parent, index);
1494     setModified(true);
1495     m_viewlistModified = true;
1496 }
1497 
removeViewListItem(View *,const ViewListItem * item)1498 void MainDocument::removeViewListItem(View */*view*/, const ViewListItem *item)
1499 {
1500     // FIXME callers should take care that they now get a signal even if originating from themselves
1501     emit viewListItemRemoved(item);
1502     setModified(true);
1503     m_viewlistModified = true;
1504 }
1505 
isLoading() const1506 bool MainDocument::isLoading() const
1507 {
1508     return m_isLoading || KoDocument::isLoading();
1509 }
1510 
setModified(bool mod)1511 void MainDocument::setModified(bool mod)
1512 {
1513     debugPlan<<mod<<m_viewlistModified;
1514     KoDocument::setModified(mod || m_viewlistModified); // Must always call to activate autosave
1515 }
1516 
slotViewlistModified()1517 void MainDocument::slotViewlistModified()
1518 {
1519     if (! m_viewlistModified) {
1520         m_viewlistModified = true;
1521     }
1522     setModified(true);  // Must always call to activate autosave
1523 }
1524 
1525 // called after user has created a new project in welcome view
slotProjectCreated()1526 void MainDocument::slotProjectCreated()
1527 {
1528     if (url().isEmpty() && !m_project->name().isEmpty()) {
1529         setUrl(QUrl(m_project->name() + ".plan"));
1530     }
1531     if (m_project->scheduleManagers().isEmpty()) {
1532         ScheduleManager *sm = m_project->createScheduleManager();
1533         sm->setAllowOverbooking(false);
1534         sm->setSchedulingMode(ScheduleManager::AutoMode);
1535     }
1536     Calendar *week = nullptr;
1537     if (KPlatoSettings::generateWeek()) {
1538         bool always = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::Always;
1539         bool ifnone = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::NoneExists;
1540         if (always || (ifnone && m_project->calendarCount() == 0)) {
1541             // create a calendar
1542             week = new Calendar(i18nc("Base calendar name", "Base"));
1543             m_project->addCalendar(week);
1544 
1545             CalendarDay vd(CalendarDay::NonWorking);
1546 
1547             for (int i = Qt::Monday; i <= Qt::Sunday; ++i) {
1548                 if (m_config.isWorkingday(i)) {
1549                     CalendarDay wd(CalendarDay::Working);
1550                     TimeInterval ti(m_config.dayStartTime(i), m_config.dayLength(i));
1551                     wd.addInterval(ti);
1552                     week->setWeekday(i, wd);
1553                 } else {
1554                     week->setWeekday(i, vd);
1555                 }
1556             }
1557             m_project->setDefaultCalendar(week);
1558         }
1559     }
1560 #ifdef HAVE_KHOLIDAYS
1561     if (KPlatoSettings::generateHolidays()) {
1562         bool inweek = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::InWeekCalendar;
1563         bool subcalendar = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSubCalendar;
1564         bool separate = week == 0 || KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSeparateCalendar;
1565 
1566         Calendar *holiday = nullptr;
1567         if (inweek) {
1568             holiday = week;
1569             week->setDefault(true);
1570             debugPlan<<"in week";
1571         } else if (subcalendar) {
1572             holiday = new Calendar(i18n("Holidays"));
1573             m_project->addCalendar(holiday, week);
1574             debugPlan<<"subcalendar";
1575         } else if (separate) {
1576             holiday = new Calendar(i18n("Holidays"));
1577             m_project->addCalendar(holiday);
1578             debugPlan<<"separate";
1579         } else {
1580             Q_ASSERT(false); // something wrong
1581         }
1582         debugPlan<<KPlatoSettings::region();
1583         if (holiday == 0) {
1584             warnPlan<<Q_FUNC_INFO<<"Failed to generate holidays. Bad option:"<<KPlatoSettings::generateHolidaysChoice();
1585             return;
1586         }
1587         holiday->setHolidayRegion(KPlatoSettings::region());
1588         m_project->setDefaultCalendar(holiday);
1589     }
1590 #endif
1591 }
1592 
1593 // creates a "new" project from current project (new ids etc)
createNewProject()1594 void MainDocument::createNewProject()
1595 {
1596     setEmpty();
1597     clearUndoHistory();
1598     setModified(false);
1599     resetURL();
1600     KoDocumentInfo *info = documentInfo();
1601     info->resetMetaData();
1602     info->setProperty("title", "");
1603     setTitleModified();
1604 
1605     m_project->generateUniqueNodeIds();
1606     Duration dur = m_project->constraintEndTime() - m_project->constraintStartTime();
1607     m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime));
1608     m_project->setConstraintEndTime(m_project->constraintStartTime() +  dur);
1609 
1610     while (m_project->numScheduleManagers() > 0) {
1611         foreach (ScheduleManager *sm, m_project->allScheduleManagers()) {
1612             if (sm->childCount() > 0) {
1613                 continue;
1614             }
1615             if (sm->expected()) {
1616                 sm->expected()->setDeleted(true);
1617                 sm->setExpected(0);
1618             }
1619             m_project->takeScheduleManager(sm);
1620             delete sm;
1621         }
1622     }
1623     foreach (Schedule *s, m_project->schedules()) {
1624         m_project->takeSchedule(s);
1625         delete s;
1626     }
1627     foreach (Node *n, m_project->allNodes()) {
1628         foreach (Schedule *s, n->schedules()) {
1629             n->takeSchedule(s);
1630             delete s;
1631         }
1632     }
1633     foreach (Resource *r, m_project->resourceList()) {
1634         foreach (Schedule *s, r->schedules()) {
1635             r->takeSchedule(s);
1636             delete s;
1637         }
1638     }
1639 }
1640 
setIsTaskModule(bool value)1641 void MainDocument::setIsTaskModule(bool value)
1642 {
1643     m_isTaskModule = value;
1644 }
1645 
isTaskModule() const1646 bool MainDocument::isTaskModule() const
1647 {
1648     return m_isTaskModule;
1649 }
1650 }  //KPlato namespace
1651