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