File indexing completed on 2024-04-28 16:24:36

0001 /* This file is part of the KDE project
0002  * Copyright (C) 1998, 1999, 2000 Torben Weis <weis@kde.org>
0003  * Copyright (C) 2004, 2010, 2012 Dag Andersen <danders@get2net.dk>
0004  * Copyright (C) 2006 Raphael Langerhorst <raphael.langerhorst@kdemail.net>
0005  * Copyright (C) 2007 Thorsten Zachmann <zachmann@kde.org>
0006  * Copyright (C) 2019 Dag Andersen <danders@get2net.dk>
0007  * 
0008  * This library is free software; you can redistribute it and/or
0009  * modify it under the terms of the GNU Library General Public
0010  * License as published by the Free Software Foundation; either
0011  * version 2 of the License, or (at your option) any later version.
0012  * 
0013  * This library is distributed in the hope that it will be useful,
0014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0016  * Library General Public License for more details.
0017  * 
0018  * You should have received a copy of the GNU Library General Public License
0019  * along with this library; see the file COPYING.LIB.  If not, write to
0020  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0021  * Boston, MA 02110-1301, USA.
0022  */
0023 
0024 // clazy:excludeall=qstring-arg
0025 #include "kptmaindocument.h"
0026 #include "kptpart.h"
0027 #include "kptview.h"
0028 #include "kptfactory.h"
0029 #include "kptproject.h"
0030 #include "kptlocale.h"
0031 #include "kptresource.h"
0032 #include "kptcontext.h"
0033 #include "kptschedulerpluginloader.h"
0034 #include "kptschedulerplugin.h"
0035 #include "kptbuiltinschedulerplugin.h"
0036 #include "kptschedule.h"
0037 #include "kptcommand.h"
0038 #include "calligraplansettings.h"
0039 #include "kpttask.h"
0040 #include "KPlatoXmlLoader.h"
0041 #include "XmlSaveContext.h"
0042 #include "kptpackage.h"
0043 #include "kptdebug.h"
0044 
0045 #include <KoStore.h>
0046 #include <KoXmlReader.h>
0047 #include <KoStoreDevice.h>
0048 #include <KoOdfReadStore.h>
0049 #include <KoUpdater.h>
0050 #include <KoProgressUpdater.h>
0051 #include <KoDocumentInfo.h>
0052 
0053 #include <QApplication>
0054 #include <QPainter>
0055 #include <QDir>
0056 #include <QMutableMapIterator>
0057 #include <QTemporaryFile>
0058 
0059 #include <klocalizedstring.h>
0060 #include <kmessagebox.h>
0061 #include <KIO/CopyJob>
0062 #include <KDirWatch>
0063 
0064 #include <kundo2command.h>
0065 
0066 #ifdef HAVE_KHOLIDAYS
0067 #include <KHolidays/HolidayRegion>
0068 #endif
0069 
0070 namespace KPlato
0071 {
0072 
0073 MainDocument::MainDocument(KoPart *part)
0074         : KoDocument(part),
0075         m_project(0),
0076         m_context(0), m_xmlLoader(),
0077         m_loadingTemplate(false),
0078         m_loadingSharedResourcesTemplate(false),
0079         m_viewlistModified(false),
0080         m_checkingForWorkPackages(false),
0081         m_loadingSharedProject(false),
0082         m_skipSharedProjects(false),
0083         m_isLoading(false),
0084         m_isTaskModule(false),
0085         m_calculationCommand(nullptr),
0086         m_currentCalculationManager(nullptr),
0087         m_nextCalculationManager(nullptr),
0088         m_taskModulesWatch(nullptr)
0089 {
0090     Q_ASSERT(part);
0091     setAlwaysAllowSaving(true);
0092     m_config.setReadWrite(isReadWrite());
0093 
0094     loadSchedulerPlugins();
0095 
0096     setProject(new Project(m_config)); // after config & plugins are loaded
0097     m_project->setId(m_project->uniqueNodeId());
0098     m_project->registerNodeId(m_project); // register myself
0099 
0100     connect(this, &MainDocument::insertSharedProject, this, &MainDocument::slotInsertSharedProject);
0101 }
0102 
0103 
0104 MainDocument::~MainDocument()
0105 {
0106     qDeleteAll(m_schedulerPlugins);
0107     if (m_project) {
0108         m_project->deref(); // deletes if last user
0109     }
0110     qDeleteAll(m_mergedPackages);
0111     delete m_context;
0112     delete m_calculationCommand;
0113 }
0114 
0115 void MainDocument::initEmpty()
0116 {
0117     KoDocument::initEmpty();
0118     setProject(new Project(m_config));
0119 }
0120 
0121 void MainDocument::slotNodeChanged(Node *node, int property)
0122 {
0123     switch (property) {
0124         case Node::TypeProperty:
0125         case Node::ResourceRequestProperty:
0126         case Node::ConstraintTypeProperty:
0127         case Node::StartConstraintProperty:
0128         case Node::EndConstraintProperty:
0129         case Node::PriorityProperty:
0130         case Node::EstimateProperty:
0131         case Node::EstimateRiskProperty:
0132             setCalculationNeeded();
0133             break;
0134         case Node::EstimateOptimisticProperty:
0135         case Node::EstimatePessimisticProperty:
0136             if (node->estimate()->risktype() != Estimate::Risk_None) {
0137                 setCalculationNeeded();
0138             }
0139             break;
0140         default:
0141             break;
0142     }
0143 }
0144 
0145 void MainDocument::slotScheduleManagerChanged(ScheduleManager *sm, int property)
0146 {
0147     if (sm->schedulingMode() == ScheduleManager::AutoMode) {
0148         switch (property) {
0149             case ScheduleManager::DirectionProperty:
0150             case ScheduleManager::OverbookProperty:
0151             case ScheduleManager::DistributionProperty:
0152             case ScheduleManager::SchedulingModeProperty:
0153             case ScheduleManager::GranularityProperty:
0154                 setCalculationNeeded();
0155                 break;
0156             default:
0157                 break;
0158         }
0159     }
0160 }
0161 
0162 void MainDocument::setCalculationNeeded()
0163 {
0164     for (ScheduleManager *sm : m_project->allScheduleManagers()) {
0165         if (sm->isBaselined()) {
0166             continue;
0167         }
0168         if (sm->schedulingMode() == ScheduleManager::AutoMode) {
0169             m_nextCalculationManager = sm;
0170             break;
0171         }
0172     }
0173     if (!m_currentCalculationManager) {
0174         m_currentCalculationManager = m_nextCalculationManager;
0175         m_nextCalculationManager = nullptr;
0176 
0177         QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
0178     }
0179 }
0180 
0181 void MainDocument::slotStartCalculation()
0182 {
0183     if (m_currentCalculationManager) {
0184         m_calculationCommand = new CalculateScheduleCmd(*m_project, m_currentCalculationManager);
0185         m_calculationCommand->redo();
0186     }
0187 }
0188 
0189 void MainDocument::slotCalculationFinished(Project *p, ScheduleManager *sm)
0190 {
0191     if (sm != m_currentCalculationManager) {
0192         return;
0193     }
0194     delete m_calculationCommand;
0195     m_calculationCommand = nullptr;
0196     m_currentCalculationManager = m_nextCalculationManager;
0197     m_nextCalculationManager = nullptr;
0198     if (m_currentCalculationManager) {
0199         QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
0200     }
0201 }
0202 
0203 void MainDocument::setReadWrite(bool rw)
0204 {
0205     m_config.setReadWrite(rw);
0206     KoDocument::setReadWrite(rw);
0207 }
0208 
0209 void MainDocument::loadSchedulerPlugins()
0210 {
0211     // Add built-in scheduler
0212     addSchedulerPlugin("Built-in", new BuiltinSchedulerPlugin(this));
0213 
0214     // Add all real scheduler plugins
0215     SchedulerPluginLoader *loader = new SchedulerPluginLoader(this);
0216     connect(loader, &SchedulerPluginLoader::pluginLoaded, this, &MainDocument::addSchedulerPlugin);
0217     loader->loadAllPlugins();
0218 }
0219 
0220 void MainDocument::addSchedulerPlugin(const QString &key, SchedulerPlugin *plugin)
0221 {
0222     debugPlan<<plugin;
0223     m_schedulerPlugins[key] = plugin;
0224 }
0225 
0226 void MainDocument::configChanged()
0227 {
0228     //m_project->setConfig(m_config);
0229 }
0230 
0231 void MainDocument::setProject(Project *project)
0232 {
0233     if (m_project) {
0234         delete m_project;
0235     }
0236     m_project = project;
0237     if (m_project) {
0238         connect(m_project, &Project::projectChanged, this, &MainDocument::changed);
0239 //        m_project->setConfig(config());
0240         m_project->setSchedulerPlugins(m_schedulerPlugins);
0241 
0242         // For auto scheduling
0243         delete m_calculationCommand;
0244         m_calculationCommand = nullptr;
0245         m_currentCalculationManager = nullptr;
0246         m_nextCalculationManager = nullptr;
0247         connect(m_project, &Project::nodeAdded, this, &MainDocument::setCalculationNeeded);
0248         connect(m_project, &Project::nodeRemoved, this, &MainDocument::setCalculationNeeded);
0249         connect(m_project, &Project::relationAdded, this, &MainDocument::setCalculationNeeded);
0250         connect(m_project, &Project::relationRemoved, this, &MainDocument::setCalculationNeeded);
0251         connect(m_project, &Project::calendarChanged, this, &MainDocument::setCalculationNeeded);
0252         connect(m_project, &Project::defaultCalendarChanged, this, &MainDocument::setCalculationNeeded);
0253         connect(m_project, &Project::calendarAdded, this, &MainDocument::setCalculationNeeded);
0254         connect(m_project, &Project::calendarRemoved, this, &MainDocument::setCalculationNeeded);
0255         connect(m_project, &Project::scheduleManagerChanged, this, &MainDocument::slotScheduleManagerChanged);
0256         connect(m_project, &Project::nodeChanged, this, &MainDocument::slotNodeChanged);
0257         connect(m_project, &Project::sigCalculationFinished, this, &MainDocument::slotCalculationFinished);
0258     }
0259 
0260     QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
0261     if (!dir.isEmpty()) {
0262         dir += "/taskmodules";
0263         m_project->setLocalTaskModulesPath(QUrl::fromLocalFile(dir));
0264     }
0265     setTaskModulesWatch();
0266     connect(project, &Project::taskModulesChanged, this, &MainDocument::setTaskModulesWatch);
0267 
0268     emit changed();
0269 }
0270 
0271 void MainDocument::setTaskModulesWatch()
0272 {
0273     delete m_taskModulesWatch;
0274     m_taskModulesWatch = new KDirWatch(this);
0275     for (const QUrl &url : m_project->taskModules()) {
0276         m_taskModulesWatch->addDir(url.toLocalFile());
0277     }
0278     connect(m_taskModulesWatch, &KDirWatch::dirty, this, &MainDocument::taskModuleDirChanged);
0279 }
0280 
0281 void MainDocument::taskModuleDirChanged()
0282 {
0283     // HACK to trigger update FIXME
0284     m_project->setUseLocalTaskModules(m_project->useLocalTaskModules());
0285 }
0286 
0287 bool MainDocument::loadOdf(KoOdfReadStore &odfStore)
0288 {
0289     warnPlan<< "OpenDocument not supported, let's try native xml format";
0290     return loadXML(odfStore.contentDoc(), 0); // We have only one format, so try to load that!
0291 }
0292 
0293 bool MainDocument::loadXML(const KoXmlDocument &document, KoStore*)
0294 {
0295     QPointer<KoUpdater> updater;
0296     if (progressUpdater()) {
0297         updater = progressUpdater()->startSubtask(1, "Plan::Part::loadXML");
0298         updater->setProgress(0);
0299         m_xmlLoader.setUpdater(updater);
0300     }
0301 
0302     QString value;
0303     KoXmlElement plan = document.documentElement();
0304 
0305     // Check if this is the right app
0306     value = plan.attribute("mime", QString());
0307     if (value.isEmpty()) {
0308         errorPlan << "No mime type specified!";
0309         setErrorMessage(i18n("Invalid document. No mimetype specified."));
0310         return false;
0311     }
0312     if (value == "application/x-vnd.kde.kplato") {
0313         if (updater) {
0314             updater->setProgress(5);
0315         }
0316         m_xmlLoader.setMimetype(value);
0317         QString message;
0318         Project *newProject = new Project(m_config, false);
0319         KPlatoXmlLoader loader(m_xmlLoader, newProject);
0320         bool ok = loader.load(plan);
0321         if (ok) {
0322             setProject(newProject);
0323             setModified(false);
0324             debugPlan<<newProject->schedules();
0325             // Cleanup after possible bug:
0326             // There should *not* be any deleted schedules (or with parent == 0)
0327             foreach (Node *n, newProject->nodeDict()) {
0328                 foreach (Schedule *s, n->schedules()) {
0329                     if (s->isDeleted()) { // true also if parent == 0
0330                         errorPlan<<n->name()<<s;
0331                         n->takeSchedule(s);
0332                         delete s;
0333                     }
0334                 }
0335             }
0336         } else {
0337             setErrorMessage(loader.errorMessage());
0338             delete newProject;
0339         }
0340         if (updater) {
0341             updater->setProgress(100); // the rest is only processing, not loading
0342         }
0343         emit changed();
0344         return ok;
0345     }
0346     if (value != "application/x-vnd.kde.plan") {
0347         errorPlan << "Unknown mime type " << value;
0348         setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan, got %1", value));
0349         return false;
0350     }
0351     QString syntaxVersion = plan.attribute("version", PLAN_FILE_SYNTAX_VERSION);
0352     m_xmlLoader.setVersion(syntaxVersion);
0353     if (syntaxVersion > PLAN_FILE_SYNTAX_VERSION) {
0354         KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
0355                       0, i18n("This document was created with a newer version of Plan (syntax version: %1)\n"
0356                                "Opening it in this version of Plan will lose some information.", syntaxVersion),
0357                       i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
0358         if (ret == KMessageBox::Cancel) {
0359             setErrorMessage("USER_CANCELED");
0360             return false;
0361         }
0362     }
0363     if (updater) updater->setProgress(5);
0364 /*
0365 #ifdef KOXML_USE_QDOM
0366     int numNodes = plan.childNodes().count();
0367 #else
0368     int numNodes = plan.childNodesCount();
0369 #endif
0370 */
0371 #if 0
0372 This test does not work any longer. KoXml adds a couple of elements not present in the file!!
0373     if (numNodes > 2) {
0374         //TODO: Make a proper bitching about this
0375         debugPlan <<"*** Error ***";
0376         debugPlan <<"  Children count should be maximum 2, but is" << numNodes;
0377         return false;
0378     }
0379 #endif
0380     m_xmlLoader.startLoad();
0381     KoXmlNode n = plan.firstChild();
0382     for (; ! n.isNull(); n = n.nextSibling()) {
0383         if (! n.isElement()) {
0384             continue;
0385         }
0386         KoXmlElement e = n.toElement();
0387         if (e.tagName() == "project") {
0388             Project *newProject = new Project(m_config, true);
0389             m_xmlLoader.setProject(newProject);
0390             if (newProject->load(e, m_xmlLoader)) {
0391                 if (newProject->id().isEmpty()) {
0392                     newProject->setId(newProject->uniqueNodeId());
0393                     newProject->registerNodeId(newProject);
0394                 }
0395                 // The load went fine. Throw out the old project
0396                 setProject(newProject);
0397                 // Cleanup after possible bug:
0398                 // There should *not* be any deleted schedules (or with parent == 0)
0399                 foreach (Node *n, newProject->nodeDict()) {
0400                     foreach (Schedule *s, n->schedules()) {
0401                         if (s->isDeleted()) { // true also if parent == 0
0402                             errorPlan<<n->name()<<s;
0403                             n->takeSchedule(s);
0404                             delete s;
0405                         }
0406                     }
0407                 }
0408             } else {
0409                 delete newProject;
0410                 m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of project failed");
0411                 //TODO add some ui here
0412             }
0413         }
0414     }
0415     m_xmlLoader.stopLoad();
0416 
0417     if (updater) updater->setProgress(100); // the rest is only processing, not loading
0418 
0419     setModified(false);
0420     emit changed();
0421     return true;
0422 }
0423 
0424 QDomDocument MainDocument::saveXML()
0425 {
0426     debugPlan;
0427     // Save the project
0428     XmlSaveContext context(m_project);
0429     context.save();
0430 
0431     return context.document;
0432 }
0433 
0434 QDomDocument MainDocument::saveWorkPackageXML(const Node *node, long id, Resource *resource)
0435 {
0436     debugPlanWp<<resource<<node;
0437     QDomDocument document("plan");
0438 
0439     document.appendChild(document.createProcessingInstruction(
0440                 "xml",
0441     "version=\"1.0\" encoding=\"UTF-8\""));
0442 
0443     QDomElement doc = document.createElement("planwork");
0444     doc.setAttribute("editor", "Plan");
0445     doc.setAttribute("mime", "application/x-vnd.kde.plan.work");
0446     doc.setAttribute("version", PLANWORK_FILE_SYNTAX_VERSION);
0447     doc.setAttribute("plan-version", PLAN_FILE_SYNTAX_VERSION);
0448     document.appendChild(doc);
0449 
0450     // Work package info
0451     QDomElement wp = document.createElement("workpackage");
0452     if (resource) {
0453         wp.setAttribute("owner", resource->name());
0454         wp.setAttribute("owner-id", resource->id());
0455     }
0456     wp.setAttribute("time-tag", QDateTime::currentDateTime().toString(Qt::ISODate));
0457     wp.setAttribute("save-url", m_project->workPackageInfo().retrieveUrl.toString(QUrl::None));
0458     wp.setAttribute("load-url", m_project->workPackageInfo().publishUrl.toString(QUrl::None));
0459     debugPlanWp<<"publish:"<<m_project->workPackageInfo().publishUrl.toString(QUrl::None);
0460     debugPlanWp<<"retrieve:"<<m_project->workPackageInfo().retrieveUrl.toString(QUrl::None);
0461     doc.appendChild(wp);
0462 
0463     // Save the project
0464     m_project->saveWorkPackageXML(doc, node, id);
0465 
0466     return document;
0467 }
0468 
0469 bool MainDocument::saveWorkPackageToStream(QIODevice *dev, const Node *node, long id, Resource *resource)
0470 {
0471     QDomDocument doc = saveWorkPackageXML(node, id, resource);
0472     // Save to buffer
0473     QByteArray s = doc.toByteArray(); // utf8 already
0474     dev->open(QIODevice::WriteOnly);
0475     int nwritten = dev->write(s.data(), s.size());
0476     if (nwritten != (int)s.size()) {
0477         warnPlanWp<<"wrote:"<<nwritten<<"- expected:"<< s.size();
0478     }
0479     return nwritten == (int)s.size();
0480 }
0481 
0482 bool MainDocument::saveWorkPackageFormat(const QString &file, const Node *node, long id, Resource *resource)
0483 {
0484     debugPlanWp <<"Saving to store";
0485 
0486     KoStore::Backend backend = KoStore::Zip;
0487 #ifdef QCA2
0488 /*    if (d->m_specialOutputFlag == SaveEncrypted) {
0489         backend = KoStore::Encrypted;
0490         debugPlan <<"Saving using encrypted backend.";
0491     }*/
0492 #endif
0493 
0494     QByteArray mimeType = "application/x-vnd.kde.plan.work";
0495     debugPlanWp <<"MimeType=" << mimeType;
0496 
0497     KoStore *store = KoStore::createStore(file, KoStore::Write, mimeType, backend);
0498 /*    if (d->m_specialOutputFlag == SaveEncrypted && !d->m_password.isNull()) {
0499         store->setPassword(d->m_password);
0500     }*/
0501     if (store->bad()) {
0502         setErrorMessage(i18n("Could not create the workpackage file for saving: %1", file)); // more details needed?
0503         delete store;
0504         return false;
0505     }
0506     // Tell KoStore not to touch the file names
0507 
0508 
0509     if (! store->open("root")) {
0510         setErrorMessage(i18n("Not able to write '%1'. Partition full?", QString("maindoc.xml")));
0511         delete store;
0512         return false;
0513     }
0514     KoStoreDevice dev(store);
0515     if (!saveWorkPackageToStream(&dev, node, id, resource) || !store->close()) {
0516         errorPlanWp <<"saveToStream failed";
0517         delete store;
0518         return false;
0519     }
0520     node->documents().saveToStore(store);
0521 
0522     debugPlanWp <<"Saving done of url:" << file;
0523     if (!store->finalize()) {
0524         delete store;
0525         return false;
0526     }
0527     // Success
0528     delete store;
0529 
0530     return true;
0531 }
0532 
0533 bool MainDocument::saveWorkPackageUrl(const QUrl &_url, const Node *node, long id, Resource *resource)
0534 {
0535     debugPlanWp<<_url;
0536     QApplication::setOverrideCursor(Qt::WaitCursor);
0537     emit statusBarMessage(i18n("Saving..."));
0538     bool ret = false;
0539     ret = saveWorkPackageFormat(_url.path(), node, id, resource); // kzip don't handle file://
0540     QApplication::restoreOverrideCursor();
0541     emit clearStatusBarMessage();
0542     return ret;
0543 }
0544 
0545 bool MainDocument::loadWorkPackage(Project &project, const QUrl &url)
0546 {
0547     debugPlanWp<<url;
0548     if (! url.isLocalFile()) {
0549         warnPlanWp<<Q_FUNC_INFO<<"TODO: download if url not local";
0550         return false;
0551     }
0552     KoStore *store = KoStore::createStore(url.path(), KoStore::Read, "", KoStore::Auto);
0553     if (store->bad()) {
0554 //        d->lastErrorMessage = i18n("Not a valid Calligra file: %1", file);
0555         errorPlanWp<<"bad store"<<url.toDisplayString();
0556         delete store;
0557 //        QApplication::restoreOverrideCursor();
0558         return false;
0559     }
0560     if (! store->open("root")) { // "old" file format (maindoc.xml)
0561         // i18n("File does not have a maindoc.xml: %1", file);
0562         errorPlanWp<<"No root"<<url.toDisplayString();
0563         delete store;
0564 //        QApplication::restoreOverrideCursor();
0565         return false;
0566     }
0567     Package *package = 0;
0568     KoXmlDocument doc;
0569     QString errorMsg; // Error variables for QDomDocument::setContent
0570     int errorLine, errorColumn;
0571     bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
0572     if (! ok) {
0573         errorPlanWp << "Parsing error in " << url.url() << "! Aborting!" << endl
0574                 << " In line: " << errorLine << ", column: " << errorColumn << endl
0575                 << " Error message: " << errorMsg;
0576         //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));
0577     } else {
0578         package = loadWorkPackageXML(project, store->device(), doc, url);
0579         if (package) {
0580             package->url = url;
0581             m_workpackages.insert(package->timeTag, package);
0582             if (!m_mergedPackages.contains(package->timeTag)) {
0583                 m_mergedPackages[package->timeTag] = package->project; // register this for next time
0584             }
0585         } else {
0586             ok = false;
0587         }
0588     }
0589     store->close();
0590     //###
0591     if (ok && package && package->settings.documents) {
0592         ok = extractFiles(store, package);
0593     }
0594     delete store;
0595     if (! ok) {
0596 //        QApplication::restoreOverrideCursor();
0597         return false;
0598     }
0599     return true;
0600 }
0601 
0602 Package *MainDocument::loadWorkPackageXML(Project &project, QIODevice *, const KoXmlDocument &document, const QUrl &url)
0603 {
0604     QString value;
0605     bool ok = true;
0606     Project *proj = 0;
0607     Package *package = 0;
0608     KoXmlElement plan = document.documentElement();
0609 
0610     // Check if this is the right app
0611     value = plan.attribute("mime", QString());
0612     if (value.isEmpty()) {
0613         errorPlanWp<<Q_FUNC_INFO<<"No mime type specified!";
0614         setErrorMessage(i18n("Invalid document. No mimetype specified."));
0615         return 0;
0616     } else if (value == "application/x-vnd.kde.kplato.work") {
0617         m_xmlLoader.setMimetype(value);
0618         m_xmlLoader.setWorkVersion(plan.attribute("version", "0.0.0"));
0619         proj = new Project();
0620         KPlatoXmlLoader loader(m_xmlLoader, proj);
0621         ok = loader.loadWorkpackage(plan);
0622         if (! ok) {
0623             setErrorMessage(loader.errorMessage());
0624             delete proj;
0625             return 0;
0626         }
0627         package = loader.package();
0628         package->timeTag = QDateTime::fromString(loader.timeTag(), Qt::ISODate);
0629     } else if (value != "application/x-vnd.kde.plan.work") {
0630         errorPlanWp << "Unknown mime type " << value;
0631         setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan.work, got %1", value));
0632         return 0;
0633     } else {
0634         if (plan.attribute("editor") != QStringLiteral("PlanWork")) {
0635             warnPlanWp<<"Skipped work package file not generated with PlanWork:"<<plan.attribute("editor")<<url;
0636             return nullptr;
0637         }
0638         QString syntaxVersion = plan.attribute("version", "0.0.0");
0639         m_xmlLoader.setWorkVersion(syntaxVersion);
0640         if (syntaxVersion > PLANWORK_FILE_SYNTAX_VERSION) {
0641             KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
0642                     0, i18n("This document was created with a newer version of PlanWork (syntax version: %1)\n"
0643                     "Opening it in this version of PlanWork will lose some information.", syntaxVersion),
0644                     i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
0645             if (ret == KMessageBox::Cancel) {
0646                 setErrorMessage("USER_CANCELED");
0647                 return 0;
0648             }
0649         }
0650         m_xmlLoader.setVersion(plan.attribute("plan-version", PLAN_FILE_SYNTAX_VERSION));
0651         m_xmlLoader.startLoad();
0652         proj = new Project();
0653         package = new Package();
0654         package->project = proj;
0655         KoXmlNode n = plan.firstChild();
0656         for (; ! n.isNull(); n = n.nextSibling()) {
0657             if (! n.isElement()) {
0658                 continue;
0659             }
0660             KoXmlElement e = n.toElement();
0661             if (e.tagName() == "project") {
0662                 m_xmlLoader.setProject(proj);
0663                 ok = proj->load(e, m_xmlLoader);
0664                 if (! ok) {
0665                     m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of work package failed");
0666                     warnPlanWp<<"Skip workpackage:"<<"Loading project failed";
0667                     //TODO add some ui here
0668                 }
0669             } else if (e.tagName() == "workpackage") {
0670                 package->timeTag = QDateTime::fromString(e.attribute("time-tag"), Qt::ISODate);
0671                 package->ownerId = e.attribute("owner-id");
0672                 package->ownerName = e.attribute("owner");
0673                 debugPlan<<"workpackage:"<<package->timeTag<<package->ownerId<<package->ownerName;
0674                 KoXmlElement elem;
0675                 forEachElement(elem, e) {
0676                     if (elem.tagName() != "settings") {
0677                         continue;
0678                     }
0679                     package->settings.usedEffort = (bool)elem.attribute("used-effort").toInt();
0680                     package->settings.progress = (bool)elem.attribute("progress").toInt();
0681                     package->settings.documents = (bool)elem.attribute("documents").toInt();
0682                 }
0683             }
0684         }
0685         if (proj->numChildren() > 0) {
0686             package->task = static_cast<Task*>(proj->childNode(0));
0687             package->toTask = qobject_cast<Task*>(m_project->findNode(package->task->id()));
0688             WorkPackage &wp = package->task->workPackage();
0689             if (wp.ownerId().isEmpty()) {
0690                 wp.setOwnerId(package->ownerId);
0691                 wp.setOwnerName(package->ownerName);
0692             }
0693             if (wp.ownerId() != package->ownerId) {
0694                 warnPlanWp<<"Current owner:"<<wp.ownerName()<<"not the same as package owner:"<<package->ownerName;
0695             }
0696             debugPlanWp<<"Task set:"<<package->task->name();
0697         }
0698         m_xmlLoader.stopLoad();
0699     }
0700     if (ok && proj->id() != project.id()) {
0701         debugPlanWp<<"Skip workpackage:"<<"Not the correct project";
0702         ok = false;
0703     }
0704     if (ok && (package->task == nullptr)) {
0705         warnPlanWp<<"Skip workpackage:"<<"No task in workpackage file";
0706         ok = false;
0707     }
0708     if (ok && (package->toTask == nullptr)) {
0709         warnPlanWp<<"Skip workpackage:"<<"Cannot find task:"<<package->task->id()<<package->task->name();
0710         ok = false;
0711     }
0712     if (ok && !package->timeTag.isValid()) {
0713         warnPlanWp<<"Work package is not time tagged:"<<package->task->name()<<package->url;
0714         ok = false;
0715     }
0716     if (ok && m_mergedPackages.contains(package->timeTag)) {
0717         debugPlanWp<<"Skip workpackage:"<<"already merged:"<<package->task->name()<<package->url;
0718         ok = false; // already merged
0719     }
0720     if (!ok) {
0721         delete proj;
0722         delete package;
0723         return nullptr;
0724     }
0725     return package;
0726 }
0727 
0728 bool MainDocument::extractFiles(KoStore *store, Package *package)
0729 {
0730     if (package->task == 0) {
0731         errorPlan<<"No task!";
0732         return false;
0733     }
0734     foreach (Document *doc, package->task->documents().documents()) {
0735         if (! doc->isValid() || doc->type() != Document::Type_Product || doc->sendAs() != Document::SendAs_Copy) {
0736             continue;
0737         }
0738         if (! extractFile(store, package, doc)) {
0739             return false;
0740         }
0741     }
0742     return true;
0743 }
0744 
0745 bool MainDocument::extractFile(KoStore *store, Package *package, const Document *doc)
0746 {
0747     QTemporaryFile tmpfile;
0748     if (! tmpfile.open()) {
0749         errorPlan<<"Failed to open temporary file";
0750         return false;
0751     }
0752     if (! store->extractFile(doc->url().fileName(), tmpfile.fileName())) {
0753         errorPlan<<"Failed to extract file:"<<doc->url().fileName()<<"to:"<<tmpfile.fileName();
0754         return false;
0755     }
0756     package->documents.insert(tmpfile.fileName(), doc->url());
0757     tmpfile.setAutoRemove(false);
0758     debugPlan<<"extracted:"<<doc->url().fileName()<<"->"<<tmpfile.fileName();
0759     return true;
0760 }
0761 
0762 void MainDocument::autoCheckForWorkPackages()
0763 {
0764     QTimer *timer = qobject_cast<QTimer*>(sender());
0765     if (m_project && m_project->workPackageInfo().checkForWorkPackages) {
0766         checkForWorkPackages(true);
0767     }
0768     if (timer && timer->interval() != 10000) {
0769         timer->stop();
0770         timer->setInterval(10000);
0771         timer->start();
0772     }
0773 }
0774 
0775 void MainDocument::checkForWorkPackages(bool keep)
0776 {
0777     if (m_checkingForWorkPackages || m_project == nullptr || m_project->numChildren() == 0 || m_project->workPackageInfo().retrieveUrl.isEmpty()) {
0778         return;
0779     }
0780     if (! keep) {
0781         qDeleteAll(m_mergedPackages);
0782         m_mergedPackages.clear();
0783     }
0784     QDir dir(m_project->workPackageInfo().retrieveUrl.path(), "*.planwork");
0785     m_infoList = dir.entryInfoList(QDir::Files | QDir::Readable, QDir::Time);
0786     checkForWorkPackage();
0787     return;
0788 }
0789 
0790 void MainDocument::checkForWorkPackage()
0791 {
0792     if (! m_infoList.isEmpty()) {
0793         m_checkingForWorkPackages = true;
0794         QUrl url = QUrl::fromLocalFile(m_infoList.takeLast().absoluteFilePath());
0795         if (!m_skipUrls.contains(url) && !loadWorkPackage(*m_project, url)) {
0796             m_skipUrls << url;
0797             debugPlanWp<<"skip url:"<<url;
0798         }
0799         if (! m_infoList.isEmpty()) {
0800             QTimer::singleShot (0, this, &MainDocument::checkForWorkPackage);
0801             return;
0802         }
0803         // Merge our workpackages
0804         if (! m_workpackages.isEmpty()) {
0805             emit workPackageLoaded();
0806         }
0807         m_checkingForWorkPackages = false;
0808     }
0809 }
0810 
0811 void MainDocument::terminateWorkPackage(const Package *package)
0812 {
0813     debugPlanWp<<package->toTask<<package->url;
0814     if (m_workpackages.value(package->timeTag) == package) {
0815         m_workpackages.remove(package->timeTag);
0816     }
0817     QFile file(package->url.path());
0818     if (! file.exists()) {
0819         warnPlanWp<<"File does not exist:"<<package->toTask<<package->url;
0820         return;
0821     }
0822     Project::WorkPackageInfo wpi = m_project->workPackageInfo();
0823     debugPlanWp<<"retrieve:"<<wpi.retrieveUrl<<"archive:"<<wpi.archiveUrl;
0824     bool rename = wpi.retrieveUrl == package->url.adjusted(QUrl::RemoveFilename);
0825     if (wpi.archiveAfterRetrieval && wpi.archiveUrl.isValid()) {
0826         QDir dir(wpi.archiveUrl.path());
0827         if (! dir.exists()) {
0828             if (! dir.mkpath(dir.path())) {
0829                 //TODO message
0830                 warnPlanWp<<"Failed to create archive directory:"<<dir.path();
0831                 return;
0832             }
0833         }
0834         QFileInfo from(file);
0835         QString name = dir.absolutePath() + '/' + from.fileName();
0836         debugPlanWp<<"rename:"<<rename;
0837         if (rename ? !file.rename(name) : !file.copy(name)) {
0838             // try to create a unique name in case name already existed
0839             debugPlanWp<<"Archive exists, create unique file name";
0840             name = dir.absolutePath() + '/';
0841             name += from.completeBaseName() + "-%1";
0842             if (! from.suffix().isEmpty()) {
0843                 name += '.' + from.suffix();
0844             }
0845             int i = 0;
0846             bool ok = false;
0847             while (! ok && i < 1000) {
0848                 ++i;
0849                 ok = rename ? QFile::rename(file.fileName(), name.arg(i)) : QFile::copy(file.fileName(), name.arg(i));
0850             }
0851             if (! ok) {
0852                 //TODO message
0853                 warnPlanWp<<"terminateWorkPackage: Failed to save"<<file.fileName();
0854             }
0855         }
0856     } else if (wpi.deleteAfterRetrieval) {
0857         if (rename) {
0858             debugPlanWp<<"removed package file:"<<file.fileName();
0859             file.remove();
0860         } else {
0861             debugPlanWp<<"package file not in 'from' dir:"<<file.fileName();
0862         }
0863     } else {
0864         warnPlanWp<<"Cannot terminate package, archive:"<<wpi.archiveUrl;
0865     }
0866 }
0867 
0868 void MainDocument::paintContent(QPainter &, const QRect &)
0869 {
0870     // Don't embed this app!!!
0871 }
0872 
0873 void MainDocument::slotViewDestroyed()
0874 {
0875 }
0876 
0877 void MainDocument::setLoadingTemplate(bool loading)
0878 {
0879     m_loadingTemplate = loading;
0880 }
0881 
0882 void MainDocument::setLoadingSharedResourcesTemplate(bool loading)
0883 {
0884     m_loadingSharedResourcesTemplate = loading;
0885 }
0886 
0887 bool MainDocument::completeLoading(KoStore *store)
0888 {
0889     // If we get here the new project is loaded and set
0890     if (m_loadingSharedProject) {
0891         // this file is loaded by another project
0892         // to read resource appointments,
0893         // so we must not load any extra stuff
0894         return true;
0895     }
0896     if (m_loadingTemplate) {
0897         //debugPlan<<"Loading template, generate unique ids";
0898         m_project->generateUniqueIds();
0899         m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime));
0900         m_project->setConstraintEndTime(m_project->constraintStartTime().addYears(2));
0901         m_project->locale()->setCurrencyLocale(QLocale::AnyLanguage, QLocale::AnyCountry);
0902         m_project->locale()->setCurrencySymbol(QString());
0903     } else if (isImporting()) {
0904         // NOTE: I don't think this is a good idea.
0905         // Let the filter generate ids for non-plan files.
0906         // If the user wants to create a new project from an old one,
0907         // he should use Tools -> Insert Project File
0908 
0909         //m_project->generateUniqueNodeIds();
0910     }
0911     if (m_loadingSharedResourcesTemplate && m_project->calendarCount() > 0) {
0912         Calendar *c = m_project->calendarAt(0);
0913         c->setTimeZone(QTimeZone::systemTimeZone());
0914     }
0915     if (m_project->useSharedResources() && !m_project->sharedResourcesFile().isEmpty() && !m_skipSharedProjects) {
0916         QUrl url = QUrl::fromLocalFile(m_project->sharedResourcesFile());
0917         if (url.isValid()) {
0918             insertResourcesFile(url, m_project->loadProjectsAtStartup() ? m_project->sharedProjectsUrl() : QUrl());
0919         }
0920     }
0921     if (store == 0) {
0922         // can happen if loading a template
0923         debugPlan<<"No store";
0924         return true; // continue anyway
0925     }
0926     delete m_context;
0927     m_context = new Context();
0928     KoXmlDocument doc;
0929     if (loadAndParse(store, "context.xml", doc)) {
0930         store->close();
0931         m_context->load(doc);
0932     } else warnPlan<<"No context";
0933     return true;
0934 }
0935 
0936 // TODO:
0937 // Due to splitting of KoDocument into a document and a part,
0938 // we simulate the old behaviour by registering all views in the document.
0939 // Find a better solution!
0940 void MainDocument::registerView(View* view)
0941 {
0942     if (view && ! m_views.contains(view)) {
0943         m_views << QPointer<View>(view);
0944     }
0945 }
0946 
0947 bool MainDocument::completeSaving(KoStore *store)
0948 {
0949     if (m_context && m_views.isEmpty()) {
0950         if (store->open("context.xml")) {
0951             // When e.g. saving as a template there are no views,
0952             // so we cannot get info from them.
0953             // Just use the context info we have in this case.
0954             KoStoreDevice dev(store);
0955             QByteArray s = m_context->document().toByteArray();
0956             (void)dev.write(s.data(), s.size());
0957             (void)store->close();
0958             return true;
0959         }
0960         return false;
0961     }
0962     foreach (View *view, m_views) {
0963         if (view) {
0964             if (store->open("context.xml")) {
0965                 if (m_context == 0) m_context = new Context();
0966                 QDomDocument doc = m_context->save(view);
0967 
0968                 KoStoreDevice dev(store);
0969                 QByteArray s = doc.toByteArray(); // this is already Utf8!
0970                 (void)dev.write(s.data(), s.size());
0971                 (void)store->close();
0972 
0973                 m_viewlistModified = false;
0974                 emit viewlistModified(false);
0975             }
0976             break;
0977         }
0978     }
0979     return true;
0980 }
0981 
0982 bool MainDocument::loadAndParse(KoStore *store, const QString &filename, KoXmlDocument &doc)
0983 {
0984     //debugPlan << "oldLoadAndParse: Trying to open " << filename;
0985 
0986     if (!store->open(filename))
0987     {
0988         warnPlan << "Entry " << filename << " not found!";
0989 //        d->lastErrorMessage = i18n("Could not find %1",filename);
0990         return false;
0991     }
0992     // Error variables for QDomDocument::setContent
0993     QString errorMsg;
0994     int errorLine, errorColumn;
0995     bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
0996     if (!ok)
0997     {
0998         errorPlan << "Parsing error in " << filename << "! Aborting!" << endl
0999             << " 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 
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 
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 
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 
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 
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 
1092 void MainDocument::clearResourceAssignments()
1093 {
1094     foreach (Resource *r, m_project->resourceList()) {
1095         r->clearExternalAppointments();
1096     }
1097 }
1098 
1099 void MainDocument::loadResourceAssignments(QUrl url)
1100 {
1101     insertSharedProjects(url);
1102     slotInsertSharedProject();
1103 }
1104 
1105 void MainDocument::insertSharedProjects(const QList<QUrl> &urls)
1106 {
1107     clearResourceAssignments();
1108     m_sharedProjectsFiles = urls;
1109     slotInsertSharedProject();
1110 }
1111 
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 
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 
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 
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 
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)
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
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 
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 
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 
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 
1506 bool MainDocument::isLoading() const
1507 {
1508     return m_isLoading || KoDocument::isLoading();
1509 }
1510 
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 
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
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)
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 
1641 void MainDocument::setIsTaskModule(bool value)
1642 {
1643     m_isTaskModule = value;
1644 }
1645 
1646 bool MainDocument::isTaskModule() const
1647 {
1648     return m_isTaskModule;
1649 }
1650 }  //KPlato namespace