diff --git a/src/kptmaindocument.cpp b/src/kptmaindocument.cpp index 21ffd419..d3122104 100644 --- a/src/kptmaindocument.cpp +++ b/src/kptmaindocument.cpp @@ -1,1654 +1,1652 @@ /* This file is part of the KDE project * Copyright (C) 1998, 1999, 2000 Torben Weis * Copyright (C) 2004, 2010, 2012 Dag Andersen * Copyright (C) 2006 Raphael Langerhorst * Copyright (C) 2007 Thorsten Zachmann * Copyright (C) 2019 Dag Andersen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // clazy:excludeall=qstring-arg #include "kptmaindocument.h" #include "kptpart.h" #include "kptview.h" #include "kptfactory.h" #include "kptproject.h" #include "kptlocale.h" #include "kptresource.h" #include "kptcontext.h" #include "kptschedulerpluginloader.h" #include "kptschedulerplugin.h" #include "kptbuiltinschedulerplugin.h" #include "kptschedule.h" #include "kptcommand.h" #include #include "calligraplansettings.h" #include "kpttask.h" #include "KPlatoXmlLoader.h" #include "XmlSaveContext.h" #include "kptpackage.h" #include "kptdebug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KHOLIDAYS #include #endif namespace KPlato { MainDocument::MainDocument(KoPart *part) : KoDocument(part), m_project(0), m_context(0), m_xmlLoader(), m_loadingTemplate(false), m_loadingSharedResourcesTemplate(false), m_viewlistModified(false), m_checkingForWorkPackages(false), m_loadingSharedProject(false), m_skipSharedProjects(false), m_isLoading(false), m_isTaskModule(false), m_calculationCommand(nullptr), m_currentCalculationManager(nullptr), m_nextCalculationManager(nullptr), m_taskModulesWatch(nullptr) { Q_ASSERT(part); setAlwaysAllowSaving(true); m_config.setReadWrite(isReadWrite()); loadSchedulerPlugins(); setProject(new Project(m_config)); // after config & plugins are loaded m_project->setId(m_project->uniqueNodeId()); m_project->registerNodeId(m_project); // register myself connect(this, &MainDocument::insertSharedProject, this, &MainDocument::slotInsertSharedProject); } MainDocument::~MainDocument() { qDeleteAll(m_schedulerPlugins); if (m_project) { m_project->deref(); // deletes if last user } qDeleteAll(m_mergedPackages); delete m_context; delete m_calculationCommand; } void MainDocument::initEmpty() { KoDocument::initEmpty(); setProject(new Project(m_config)); } void MainDocument::slotNodeChanged(Node *node, int property) { switch (property) { case Node::TypeProperty: case Node::ResourceRequestProperty: case Node::ConstraintTypeProperty: case Node::StartConstraintProperty: case Node::EndConstraintProperty: case Node::PriorityProperty: case Node::EstimateProperty: case Node::EstimateRiskProperty: setCalculationNeeded(); break; case Node::EstimateOptimisticProperty: case Node::EstimatePessimisticProperty: if (node->estimate()->risktype() != Estimate::Risk_None) { setCalculationNeeded(); } break; default: break; } } void MainDocument::slotScheduleManagerChanged(ScheduleManager *sm, int property) { if (sm->schedulingMode() == ScheduleManager::AutoMode) { switch (property) { case ScheduleManager::DirectionProperty: case ScheduleManager::OverbookProperty: case ScheduleManager::DistributionProperty: case ScheduleManager::SchedulingModeProperty: case ScheduleManager::GranularityProperty: setCalculationNeeded(); break; default: break; } } } void MainDocument::setCalculationNeeded() { for (ScheduleManager *sm : m_project->allScheduleManagers()) { if (sm->isBaselined()) { continue; } if (sm->schedulingMode() == ScheduleManager::AutoMode) { m_nextCalculationManager = sm; break; } } if (!m_currentCalculationManager) { m_currentCalculationManager = m_nextCalculationManager; m_nextCalculationManager = nullptr; QTimer::singleShot(0, this, &MainDocument::slotStartCalculation); } } void MainDocument::slotStartCalculation() { if (m_currentCalculationManager) { m_calculationCommand = new CalculateScheduleCmd(*m_project, m_currentCalculationManager); m_calculationCommand->redo(); } } void MainDocument::slotCalculationFinished(Project *p, ScheduleManager *sm) { if (sm != m_currentCalculationManager) { return; } delete m_calculationCommand; m_calculationCommand = nullptr; m_currentCalculationManager = m_nextCalculationManager; m_nextCalculationManager = nullptr; if (m_currentCalculationManager) { QTimer::singleShot(0, this, &MainDocument::slotStartCalculation); } } void MainDocument::setReadWrite(bool rw) { m_config.setReadWrite(rw); KoDocument::setReadWrite(rw); } void MainDocument::loadSchedulerPlugins() { // Add built-in scheduler addSchedulerPlugin("Built-in", new BuiltinSchedulerPlugin(this)); // Add all real scheduler plugins SchedulerPluginLoader *loader = new SchedulerPluginLoader(this); connect(loader, &SchedulerPluginLoader::pluginLoaded, this, &MainDocument::addSchedulerPlugin); loader->loadAllPlugins(); } void MainDocument::addSchedulerPlugin(const QString &key, SchedulerPlugin *plugin) { debugPlan<setConfig(m_config); } void MainDocument::setProject(Project *project) { if (m_project) { delete m_project; } m_project = project; if (m_project) { connect(m_project, &Project::projectChanged, this, &MainDocument::changed); // m_project->setConfig(config()); m_project->setSchedulerPlugins(m_schedulerPlugins); // For auto scheduling delete m_calculationCommand; m_calculationCommand = nullptr; m_currentCalculationManager = nullptr; m_nextCalculationManager = nullptr; connect(m_project, &Project::nodeAdded, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::nodeRemoved, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::relationAdded, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::relationRemoved, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::calendarChanged, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::defaultCalendarChanged, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::calendarAdded, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::calendarRemoved, this, &MainDocument::setCalculationNeeded); connect(m_project, &Project::scheduleManagerChanged, this, &MainDocument::slotScheduleManagerChanged); connect(m_project, &Project::nodeChanged, this, &MainDocument::slotNodeChanged); connect(m_project, &Project::sigCalculationFinished, this, &MainDocument::slotCalculationFinished); } m_aboutPage.setProject(project); QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (!dir.isEmpty()) { dir += "/taskmodules"; m_project->setLocalTaskModulesPath(QUrl::fromLocalFile(dir)); } setTaskModulesWatch(); connect(project, &Project::taskModulesChanged, this, &MainDocument::setTaskModulesWatch); emit changed(); } void MainDocument::setTaskModulesWatch() { delete m_taskModulesWatch; m_taskModulesWatch = new KDirWatch(this); for (const QUrl &url : m_project->taskModules()) { m_taskModulesWatch->addDir(url.toLocalFile()); } connect(m_taskModulesWatch, &KDirWatch::dirty, this, &MainDocument::taskModuleDirChanged); } void MainDocument::taskModuleDirChanged() { // HACK to trigger update FIXME m_project->setUseLocalTaskModules(m_project->useLocalTaskModules()); } bool MainDocument::loadOdf(KoOdfReadStore &odfStore) { warnPlan<< "OpenDocument not supported, let's try native xml format"; return loadXML(odfStore.contentDoc(), 0); // We have only one format, so try to load that! } bool MainDocument::loadXML(const KoXmlDocument &document, KoStore*) { QPointer updater; if (progressUpdater()) { updater = progressUpdater()->startSubtask(1, "Plan::Part::loadXML"); updater->setProgress(0); m_xmlLoader.setUpdater(updater); } QString value; KoXmlElement plan = document.documentElement(); // Check if this is the right app value = plan.attribute("mime", QString()); if (value.isEmpty()) { errorPlan << "No mime type specified!"; setErrorMessage(i18n("Invalid document. No mimetype specified.")); return false; } if (value == "application/x-vnd.kde.kplato") { if (updater) { updater->setProgress(5); } m_xmlLoader.setMimetype(value); QString message; Project *newProject = new Project(m_config, false); KPlatoXmlLoader loader(m_xmlLoader, newProject); bool ok = loader.load(plan); if (ok) { setProject(newProject); setModified(false); debugPlan<schedules(); // Cleanup after possible bug: // There should *not* be any deleted schedules (or with parent == 0) foreach (Node *n, newProject->nodeDict()) { foreach (Schedule *s, n->schedules()) { if (s->isDeleted()) { // true also if parent == 0 errorPlan<name()<takeSchedule(s); delete s; } } } } else { setErrorMessage(loader.errorMessage()); delete newProject; } if (updater) { updater->setProgress(100); // the rest is only processing, not loading } emit changed(); return ok; } if (value != "application/x-vnd.kde.plan") { errorPlan << "Unknown mime type " << value; setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan, got %1", value)); return false; } QString syntaxVersion = plan.attribute("version", PLAN_FILE_SYNTAX_VERSION); m_xmlLoader.setVersion(syntaxVersion); if (syntaxVersion > PLAN_FILE_SYNTAX_VERSION) { KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel( 0, i18n("This document was created with a newer version of Plan (syntax version: %1)\n" "Opening it in this version of Plan will lose some information.", syntaxVersion), i18n("File-Format Mismatch"), KGuiItem(i18n("Continue"))); if (ret == KMessageBox::Cancel) { setErrorMessage("USER_CANCELED"); return false; } } if (updater) updater->setProgress(5); /* #ifdef KOXML_USE_QDOM int numNodes = plan.childNodes().count(); #else int numNodes = plan.childNodesCount(); #endif */ #if 0 This test does not work any longer. KoXml adds a couple of elements not present in the file!! if (numNodes > 2) { //TODO: Make a proper bitching about this debugPlan <<"*** Error ***"; debugPlan <<" Children count should be maximum 2, but is" << numNodes; return false; } #endif m_xmlLoader.startLoad(); KoXmlNode n = plan.firstChild(); for (; ! n.isNull(); n = n.nextSibling()) { if (! n.isElement()) { continue; } KoXmlElement e = n.toElement(); if (e.tagName() == "project") { Project *newProject = new Project(m_config, true); m_xmlLoader.setProject(newProject); if (newProject->load(e, m_xmlLoader)) { if (newProject->id().isEmpty()) { newProject->setId(newProject->uniqueNodeId()); newProject->registerNodeId(newProject); } // The load went fine. Throw out the old project setProject(newProject); // Cleanup after possible bug: // There should *not* be any deleted schedules (or with parent == 0) foreach (Node *n, newProject->nodeDict()) { foreach (Schedule *s, n->schedules()) { if (s->isDeleted()) { // true also if parent == 0 errorPlan<name()<takeSchedule(s); delete s; } } } } else { delete newProject; m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of project failed"); //TODO add some ui here } } } m_xmlLoader.stopLoad(); if (updater) updater->setProgress(100); // the rest is only processing, not loading setModified(false); emit changed(); return true; } QDomDocument MainDocument::saveXML() { debugPlan; // Save the project XmlSaveContext context(m_project); context.save(); return context.document; } QDomDocument MainDocument::saveWorkPackageXML(const Node *node, long id, Resource *resource) { debugPlanWp<name()); wp.setAttribute("owner-id", resource->id()); } wp.setAttribute("time-tag", QDateTime::currentDateTime().toString(Qt::ISODate)); wp.setAttribute("save-url", m_project->workPackageInfo().retrieveUrl.toString(QUrl::None)); wp.setAttribute("load-url", m_project->workPackageInfo().publishUrl.toString(QUrl::None)); debugPlanWp<<"publish:"<workPackageInfo().publishUrl.toString(QUrl::None); debugPlanWp<<"retrieve:"<workPackageInfo().retrieveUrl.toString(QUrl::None); doc.appendChild(wp); // Save the project m_project->saveWorkPackageXML(doc, node, id); return document; } bool MainDocument::saveWorkPackageToStream(QIODevice *dev, const Node *node, long id, Resource *resource) { QDomDocument doc = saveWorkPackageXML(node, id, resource); // Save to buffer QByteArray s = doc.toByteArray(); // utf8 already dev->open(QIODevice::WriteOnly); int nwritten = dev->write(s.data(), s.size()); if (nwritten != (int)s.size()) { warnPlanWp<<"wrote:"<m_specialOutputFlag == SaveEncrypted) { backend = KoStore::Encrypted; debugPlan <<"Saving using encrypted backend."; }*/ #endif QByteArray mimeType = "application/x-vnd.kde.plan.work"; debugPlanWp <<"MimeType=" << mimeType; KoStore *store = KoStore::createStore(file, KoStore::Write, mimeType, backend); /* if (d->m_specialOutputFlag == SaveEncrypted && !d->m_password.isNull()) { store->setPassword(d->m_password); }*/ if (store->bad()) { setErrorMessage(i18n("Could not create the workpackage file for saving: %1", file)); // more details needed? delete store; return false; } // Tell KoStore not to touch the file names if (! store->open("root")) { setErrorMessage(i18n("Not able to write '%1'. Partition full?", QString("maindoc.xml"))); delete store; return false; } KoStoreDevice dev(store); if (!saveWorkPackageToStream(&dev, node, id, resource) || !store->close()) { errorPlanWp <<"saveToStream failed"; delete store; return false; } node->documents().saveToStore(store); debugPlanWp <<"Saving done of url:" << file; if (!store->finalize()) { delete store; return false; } // Success delete store; return true; } bool MainDocument::saveWorkPackageUrl(const QUrl &_url, const Node *node, long id, Resource *resource) { debugPlanWp<<_url; QApplication::setOverrideCursor(Qt::WaitCursor); emit statusBarMessage(i18n("Saving...")); bool ret = false; ret = saveWorkPackageFormat(_url.path(), node, id, resource); // kzip don't handle file:// QApplication::restoreOverrideCursor(); emit clearStatusBarMessage(); return ret; } bool MainDocument::loadWorkPackage(Project &project, const QUrl &url) { debugPlanWp<bad()) { // d->lastErrorMessage = i18n("Not a valid Calligra file: %1", file); errorPlanWp<<"bad store"<open("root")) { // "old" file format (maindoc.xml) // i18n("File does not have a maindoc.xml: %1", file); errorPlanWp<<"No root"<device(), &errorMsg, &errorLine, &errorColumn); if (! ok) { errorPlanWp << "Parsing error in " << url.url() << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg; //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)); } else { package = loadWorkPackageXML(project, store->device(), doc, url); if (package) { package->url = url; m_workpackages.insert(package->timeTag, package); if (!m_mergedPackages.contains(package->timeTag)) { m_mergedPackages[package->timeTag] = package->project; // register this for next time } } else { ok = false; } } store->close(); //### if (ok && package && package->settings.documents) { ok = extractFiles(store, package); } delete store; if (! ok) { // QApplication::restoreOverrideCursor(); return false; } return true; } Package *MainDocument::loadWorkPackageXML(Project &project, QIODevice *, const KoXmlDocument &document, const QUrl &url) { QString value; bool ok = true; Project *proj = 0; Package *package = 0; KoXmlElement plan = document.documentElement(); // Check if this is the right app value = plan.attribute("mime", QString()); if (value.isEmpty()) { errorPlanWp<timeTag = QDateTime::fromString(loader.timeTag(), Qt::ISODate); } else if (value != "application/x-vnd.kde.plan.work") { errorPlanWp << "Unknown mime type " << value; setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan.work, got %1", value)); return 0; } else { if (plan.attribute("editor") != QStringLiteral("PlanWork")) { warnPlanWp<<"Skipped work package file not generated with PlanWork:"< PLANWORK_FILE_SYNTAX_VERSION) { KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel( 0, i18n("This document was created with a newer version of PlanWork (syntax version: %1)\n" "Opening it in this version of PlanWork will lose some information.", syntaxVersion), i18n("File-Format Mismatch"), KGuiItem(i18n("Continue"))); if (ret == KMessageBox::Cancel) { setErrorMessage("USER_CANCELED"); return 0; } } m_xmlLoader.setVersion(plan.attribute("plan-version", PLAN_FILE_SYNTAX_VERSION)); m_xmlLoader.startLoad(); proj = new Project(); package = new Package(); package->project = proj; KoXmlNode n = plan.firstChild(); for (; ! n.isNull(); n = n.nextSibling()) { if (! n.isElement()) { continue; } KoXmlElement e = n.toElement(); if (e.tagName() == "project") { m_xmlLoader.setProject(proj); ok = proj->load(e, m_xmlLoader); if (! ok) { m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of work package failed"); warnPlanWp<<"Skip workpackage:"<<"Loading project failed"; //TODO add some ui here } } else if (e.tagName() == "workpackage") { package->timeTag = QDateTime::fromString(e.attribute("time-tag"), Qt::ISODate); package->ownerId = e.attribute("owner-id"); package->ownerName = e.attribute("owner"); debugPlan<<"workpackage:"<timeTag<ownerId<ownerName; KoXmlElement elem; forEachElement(elem, e) { if (elem.tagName() != "settings") { continue; } package->settings.usedEffort = (bool)elem.attribute("used-effort").toInt(); package->settings.progress = (bool)elem.attribute("progress").toInt(); package->settings.documents = (bool)elem.attribute("documents").toInt(); } } } if (proj->numChildren() > 0) { package->task = static_cast(proj->childNode(0)); package->toTask = qobject_cast(m_project->findNode(package->task->id())); WorkPackage &wp = package->task->workPackage(); if (wp.ownerId().isEmpty()) { wp.setOwnerId(package->ownerId); wp.setOwnerName(package->ownerName); } if (wp.ownerId() != package->ownerId) { warnPlanWp<<"Current owner:"<ownerName; } debugPlanWp<<"Task set:"<task->name(); } m_xmlLoader.stopLoad(); } if (ok && proj->id() != project.id()) { debugPlanWp<<"Skip workpackage:"<<"Not the correct project"; ok = false; } if (ok && (package->task == nullptr)) { warnPlanWp<<"Skip workpackage:"<<"No task in workpackage file"; ok = false; } if (ok && (package->toTask == nullptr)) { warnPlanWp<<"Skip workpackage:"<<"Cannot find task:"<task->id()<task->name(); ok = false; } if (ok && !package->timeTag.isValid()) { warnPlanWp<<"Work package is not time tagged:"<task->name()<url; ok = false; } if (ok && m_mergedPackages.contains(package->timeTag)) { debugPlanWp<<"Skip workpackage:"<<"already merged:"<task->name()<url; ok = false; // already merged } if (!ok) { delete proj; delete package; return nullptr; } return package; } bool MainDocument::extractFiles(KoStore *store, Package *package) { if (package->task == 0) { errorPlan<<"No task!"; return false; } foreach (Document *doc, package->task->documents().documents()) { if (! doc->isValid() || doc->type() != Document::Type_Product || doc->sendAs() != Document::SendAs_Copy) { continue; } if (! extractFile(store, package, doc)) { return false; } } return true; } bool MainDocument::extractFile(KoStore *store, Package *package, const Document *doc) { QTemporaryFile tmpfile; if (! tmpfile.open()) { errorPlan<<"Failed to open temporary file"; return false; } if (! store->extractFile(doc->url().fileName(), tmpfile.fileName())) { errorPlan<<"Failed to extract file:"<url().fileName()<<"to:"<documents.insert(tmpfile.fileName(), doc->url()); tmpfile.setAutoRemove(false); debugPlan<<"extracted:"<url().fileName()<<"->"<(sender()); if (m_project && m_project->workPackageInfo().checkForWorkPackages) { checkForWorkPackages(true); } if (timer && timer->interval() != 10000) { timer->stop(); timer->setInterval(10000); timer->start(); } } void MainDocument::checkForWorkPackages(bool keep) { if (m_checkingForWorkPackages || m_project == nullptr || m_project->numChildren() == 0 || m_project->workPackageInfo().retrieveUrl.isEmpty()) { return; } if (! keep) { qDeleteAll(m_mergedPackages); m_mergedPackages.clear(); } QDir dir(m_project->workPackageInfo().retrieveUrl.path(), "*.planwork"); m_infoList = dir.entryInfoList(QDir::Files | QDir::Readable, QDir::Time); checkForWorkPackage(); return; } void MainDocument::checkForWorkPackage() { if (! m_infoList.isEmpty()) { m_checkingForWorkPackages = true; QUrl url = QUrl::fromLocalFile(m_infoList.takeLast().absoluteFilePath()); if (!m_skipUrls.contains(url) && !loadWorkPackage(*m_project, url)) { m_skipUrls << url; debugPlanWp<<"skip url:"<toTask<url; if (m_workpackages.value(package->timeTag) == package) { m_workpackages.remove(package->timeTag); } QFile file(package->url.path()); if (! file.exists()) { warnPlanWp<<"File does not exist:"<toTask<url; return; } Project::WorkPackageInfo wpi = m_project->workPackageInfo(); debugPlanWp<<"retrieve:"<url.adjusted(QUrl::RemoveFilename); if (wpi.archiveAfterRetrieval && wpi.archiveUrl.isValid()) { QDir dir(wpi.archiveUrl.path()); if (! dir.exists()) { if (! dir.mkpath(dir.path())) { //TODO message warnPlanWp<<"Failed to create archive directory:"<generateUniqueIds(); m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime)); m_project->setConstraintEndTime(m_project->constraintStartTime().addYears(2)); m_project->locale()->setCurrencyLocale(QLocale::AnyLanguage, QLocale::AnyCountry); m_project->locale()->setCurrencySymbol(QString()); } else if (isImporting()) { // NOTE: I don't think this is a good idea. // Let the filter generate ids for non-plan files. // If the user wants to create a new project from an old one, // he should use Tools -> Insert Project File //m_project->generateUniqueNodeIds(); } if (m_loadingSharedResourcesTemplate && m_project->calendarCount() > 0) { Calendar *c = m_project->calendarAt(0); c->setTimeZone(QTimeZone::systemTimeZone()); } if (m_project->useSharedResources() && !m_project->sharedResourcesFile().isEmpty() && !m_skipSharedProjects) { QUrl url = QUrl::fromLocalFile(m_project->sharedResourcesFile()); if (url.isValid()) { insertResourcesFile(url, m_project->loadProjectsAtStartup() ? m_project->sharedProjectsUrl() : QUrl()); } } if (store == 0) { // can happen if loading a template debugPlan<<"No store"; return true; // continue anyway } delete m_context; m_context = new Context(); KoXmlDocument doc; if (loadAndParse(store, "context.xml", doc)) { store->close(); m_context->load(doc); } else warnPlan<<"No context"; return true; } // TODO: // Due to splitting of KoDocument into a document and a part, // we simulate the old behaviour by registering all views in the document. // Find a better solution! void MainDocument::registerView(View* view) { if (view && ! m_views.contains(view)) { m_views << QPointer(view); } } bool MainDocument::completeSaving(KoStore *store) { if (m_context && m_views.isEmpty()) { if (store->open("context.xml")) { // When e.g. saving as a template there are no views, // so we cannot get info from them. // Just use the context info we have in this case. KoStoreDevice dev(store); QByteArray s = m_context->document().toByteArray(); (void)dev.write(s.data(), s.size()); (void)store->close(); return true; } return false; } foreach (View *view, m_views) { if (view) { if (store->open("context.xml")) { if (m_context == 0) m_context = new Context(); QDomDocument doc = m_context->save(view); KoStoreDevice dev(store); QByteArray s = doc.toByteArray(); // this is already Utf8! (void)dev.write(s.data(), s.size()); (void)store->close(); m_viewlistModified = false; emit viewlistModified(false); } break; } } return true; } bool MainDocument::loadAndParse(KoStore *store, const QString &filename, KoXmlDocument &doc) { //debugPlan << "oldLoadAndParse: Trying to open " << filename; if (!store->open(filename)) { warnPlan << "Entry " << filename << " not found!"; // d->lastErrorMessage = i18n("Could not find %1",filename); return false; } // Error variables for QDomDocument::setContent QString errorMsg; int errorLine, errorColumn; bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn); if (!ok) { errorPlan << "Parsing error in " << filename << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg; /* 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));*/ store->close(); return false; } debugPlan << "File " << filename << " loaded and parsed"; return true; } void MainDocument::insertFile(const QUrl &url, Node *parent, Node *after) { Part *part = new Part(this); MainDocument *doc = new MainDocument(part); part->setDocument(doc); doc->disconnect(); // doc shall not handle feedback from openUrl() doc->setAutoSave(0); //disable doc->m_insertFileInfo.url = url; doc->m_insertFileInfo.parent = parent; doc->m_insertFileInfo.after = after; connect(doc, &KoDocument::completed, this, &MainDocument::insertFileCompleted); connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled); m_isLoading = true; doc->openUrl(url); } void MainDocument::insertFileCompleted() { debugPlan<(sender()); if (doc) { Project &p = doc->getProject(); insertProject(p, doc->m_insertFileInfo.parent, doc->m_insertFileInfo.after); doc->documentPart()->deleteLater(); // also deletes document } else { KMessageBox::error(0, i18n("Internal error, failed to insert file.")); } m_isLoading = false; } void MainDocument::insertResourcesFile(const QUrl &url, const QUrl &projects) { insertSharedProjects(projects); // prepare for insertion after shared resources m_sharedProjectsFiles.removeAll(url); // resource file is not a project Part *part = new Part(this); MainDocument *doc = new MainDocument(part); doc->m_skipSharedProjects = true; // should not have shared projects, but... part->setDocument(doc); doc->disconnect(); // doc shall not handle feedback from openUrl() doc->setAutoSave(0); //disable doc->setCheckAutoSaveFile(false); connect(doc, &KoDocument::completed, this, &MainDocument::insertResourcesFileCompleted); connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled); m_isLoading = true; doc->openUrl(url); } void MainDocument::insertResourcesFileCompleted() { debugPlanShared<(sender()); if (doc) { Project &p = doc->getProject(); mergeResources(p); m_project->setSharedResourcesLoaded(true); doc->documentPart()->deleteLater(); // also deletes document slotInsertSharedProject(); // insert shared bookings } else { KMessageBox::error(0, i18n("Internal error, failed to insert file.")); } m_isLoading = false; } void MainDocument::insertFileCancelled(const QString &error) { debugPlan<(sender()); if (doc) { doc->documentPart()->deleteLater(); // also deletes document } m_isLoading = false; } void MainDocument::clearResourceAssignments() { foreach (Resource *r, m_project->resourceList()) { r->clearExternalAppointments(); } } void MainDocument::loadResourceAssignments(QUrl url) { insertSharedProjects(url); slotInsertSharedProject(); } void MainDocument::insertSharedProjects(const QList &urls) { clearResourceAssignments(); m_sharedProjectsFiles = urls; slotInsertSharedProject(); } void MainDocument::insertSharedProjects(const QUrl &url) { m_sharedProjectsFiles.clear(); QFileInfo fi(url.path()); if (!fi.exists()) { return; } if (fi.isFile()) { m_sharedProjectsFiles = QList() << url; debugPlan<<"Get all projects in file:"<m_skipSharedProjects = true; // never load recursively part->setDocument(doc); doc->disconnect(); // doc shall not handle feedback from openUrl() doc->setAutoSave(0); //disable doc->setCheckAutoSaveFile(false); doc->m_loadingSharedProject = true; connect(doc, &KoDocument::completed, this, &MainDocument::insertSharedProjectCompleted); connect(doc, &KoDocument::canceled, this, &MainDocument::insertSharedProjectCancelled); m_isLoading = true; doc->openUrl(m_sharedProjectsFiles.takeFirst()); } void MainDocument::insertSharedProjectCompleted() { debugPlanShared<(sender()); if (doc) { Project &p = doc->getProject(); debugPlanShared<id()<<"Loaded project:"<id() && p.isScheduled(ANYSCHEDULED)) { // FIXME: improve! // find a suitable schedule ScheduleManager *sm = 0; foreach(ScheduleManager *m, p.allScheduleManagers()) { if (m->isBaselined()) { sm = m; break; } if (m->isScheduled()) { sm = m; // take the last one, more likely to be subschedule } } if (sm) { foreach(Resource *r, p.resourceList()) { Resource *res = m_project->resource(r->id()); if (res && res->isShared()) { Appointment *app = new Appointment(); app->setAuxcilliaryInfo(p.name()); foreach(const Appointment *a, r->appointments(sm->scheduleId())) { *app += *a; } if (app->isEmpty()) { delete app; } else { res->addExternalAppointment(p.id(), app); debugPlanShared<name()<<"added:"<auxcilliaryInfo()<documentPart()->deleteLater(); // also deletes document m_isLoading = false; emit insertSharedProject(); // do next file } else { KMessageBox::error(0, i18n("Internal error, failed to insert file.")); m_isLoading = false; } } void MainDocument::insertSharedProjectCancelled(const QString &error) { debugPlanShared<(sender()); if (doc) { doc->documentPart()->deleteLater(); // also deletes document } m_isLoading = false; } bool MainDocument::insertProject(Project &project, Node *parent, Node *after) { debugPlan<<&project; // make sure node ids in new project is unique also in old project QList existingIds = m_project->nodeDict().keys(); foreach (Node *n, project.allNodes()) { QString oldid = n->id(); n->setId(project.uniqueNodeId(existingIds)); project.removeId(oldid); // remove old id project.registerNodeId(n); // register new id } MacroCommand *m = new InsertProjectCmd(project, parent==0?m_project:parent, after, kundo2_i18n("Insert project")); if (m->isEmpty()) { delete m; } else { addCommand(m); } return true; } // check if calendar 'c' has children that will not be removed (normally 'Local' calendars) bool canRemoveCalendar(const Calendar *c, const QList &lst) { for (Calendar *cc : c->calendars()) { if (!lst.contains(cc)) { return false; } if (!canRemoveCalendar(cc, lst)) { return false; } } return true; } // sort parent calendars before children QList sortedRemoveCalendars(Project &shared, const QList &lst) { QList result; for (Calendar *c : lst) { if (c->isShared() && !shared.calendar(c->id())) { result << c; } result += sortedRemoveCalendars(shared, c->calendars()); } return result; } bool MainDocument::mergeResources(Project &project) { debugPlanShared<<&project; // Just in case, remove stuff not related to resources foreach(Node *n, project.childNodeIterator()) { debugPlanShared<<"Project not empty, delete node:"<name(); NodeDeleteCmd cmd(n); cmd.execute(); } foreach(ScheduleManager *m, project.scheduleManagers()) { debugPlanShared<<"Project not empty, delete schedule:"<name(); DeleteScheduleManagerCmd cmd(project, m); cmd.execute(); } foreach(Account *a, project.accounts().accountList()) { debugPlanShared<<"Project not empty, delete account:"<name(); RemoveAccountCmd cmd(project, a); cmd.execute(); } // Mark all resources / groups as shared foreach(ResourceGroup *g, project.resourceGroups()) { g->setShared(true); } foreach(Resource *r, project.resourceList()) { r->setShared(true); } // Mark all calendars shared foreach(Calendar *c, project.allCalendars()) { c->setShared(true); } // check if any shared stuff has been removed QList removedGroups; QList removedResources; QList removedCalendars; QStringList removed; foreach(ResourceGroup *g, m_project->resourceGroups()) { if (g->isShared() && !project.findResourceGroup(g->id())) { removedGroups << g; removed << i18n("Group: %1", g->name()); } } foreach(Resource *r, m_project->resourceList()) { if (r->isShared() && !project.findResource(r->id())) { removedResources << r; removed << i18n("Resource: %1", r->name()); } } removedCalendars = sortedRemoveCalendars(project, m_project->calendars()); for (Calendar *c : qAsConst(removedCalendars)) { removed << i18n("Calendar: %1", c->name()); } if (!removed.isEmpty()) { KMessageBox::ButtonCode result = KMessageBox::warningYesNoCancelList( 0, i18n("Shared resources has been removed from the shared resources file." "\nSelect how they shall be treated in this project."), removed, xi18nc("@title:window", "Shared resources"), KStandardGuiItem::remove(), KGuiItem(i18n("Convert")), KGuiItem(i18n("Keep")) ); switch (result) { case KMessageBox::Yes: // Remove for (Resource *r : qAsConst(removedResources)) { RemoveResourceCmd cmd(r); cmd.redo(); } for (ResourceGroup *g : qAsConst(removedGroups)) { if (g->resources().isEmpty()) { RemoveResourceGroupCmd cmd(m_project, g); cmd.redo(); } else { // we may have put local resource(s) in this group // so we need to keep it g->setShared(false); m_project->removeResourceGroupId(g->id()); g->setId(m_project->uniqueResourceGroupId()); m_project->insertResourceGroupId(g->id(), g); } } for (Calendar *c : qAsConst(removedCalendars)) { CalendarRemoveCmd cmd(m_project, c); cmd.redo(); } break; case KMessageBox::No: // Convert for (Resource *r : qAsConst(removedResources)) { r->setShared(false); m_project->removeResourceId(r->id()); r->setId(m_project->uniqueResourceId()); m_project->insertResourceId(r->id(), r); } for (ResourceGroup *g : qAsConst(removedGroups)) { g->setShared(false); m_project->removeResourceGroupId(g->id()); g->setId(m_project->uniqueResourceGroupId()); m_project->insertResourceGroupId(g->id(), g); } for (Calendar *c : qAsConst(removedCalendars)) { c->setShared(false); m_project->removeCalendarId(c->id()); c->setId(m_project->uniqueCalendarId()); m_project->insertCalendarId(c->id(), c); } break; case KMessageBox::Cancel: // Keep break; default: break; } } // update values of already existing objects QStringList l1; foreach(ResourceGroup *g, project.resourceGroups()) { l1 << g->id(); } QStringList l2; foreach(ResourceGroup *g, m_project->resourceGroups()) { l2 << g->id(); } debugPlanShared< removegroups; foreach(ResourceGroup *g, project.resourceGroups()) { ResourceGroup *group = m_project->findResourceGroup(g->id()); if (group) { if (!group->isShared()) { // User has probably created shared resources from this project, // so the resources exists but are local ones. // Convert to shared and do not load the group from shared. removegroups << g; group->setShared(true); debugPlanShared<<"Set group to shared:"<id(); } group->setName(g->name()); group->setType(g->type()); debugPlanShared<<"Updated group:"<id(); } } QList removeresources; foreach(Resource *r, project.resourceList()) { Resource *resource = m_project->findResource(r->id()); if (resource) { if (!resource->isShared()) { // User has probably created shared resources from this project, // so the resources exists but are local ones. // Convert to shared and do not load the resource from shared. removeresources << r; resource->setShared(true); debugPlanShared<<"Set resource to shared:"<id(); } resource->setName(r->name()); resource->setInitials(r->initials()); resource->setEmail(r->email()); resource->setType(r->type()); resource->setAutoAllocate(r->autoAllocate()); resource->setAvailableFrom(r->availableFrom()); resource->setAvailableUntil(r->availableUntil()); resource->setUnits(r->units()); resource->setNormalRate(r->normalRate()); resource->setOvertimeRate(r->overtimeRate()); QString id = r->calendar(true) ? r->calendar(true)->id() : QString(); resource->setCalendar(m_project->findCalendar(id)); id = r->account() ? r->account()->name() : QString(); resource->setAccount(m_project->accounts().findAccount(id)); resource->setRequiredIds(r->requiredIds()); resource->setTeamMemberIds(r->teamMemberIds()); debugPlanShared<<"Updated resource:"<id(); } } QList removecalendars; foreach(Calendar *c, project.allCalendars()) { Calendar *calendar = m_project->findCalendar(c->id()); if (calendar) { if (!calendar->isShared()) { // User has probably created shared resources from this project, // so the calendar exists but are local ones. // Convert to shared and do not load the resource from shared. removecalendars << c; calendar->setShared(true); debugPlanShared<<"Set calendar to shared:"<id(); } *calendar = *c; debugPlanShared<<"Updated calendar:"<id(); } } debugPlanShared<<"Remove:"<childCount() == 0) { removecalendars.removeAt(i); debugPlanShared<<"Delete calendar:"<id(); CalendarRemoveCmd cmd(&project, c); cmd.execute(); } } } for (Resource *r : qAsConst(removeresources)) { debugPlanShared<<"Delete resource:"<id(); RemoveResourceCmd cmd(r); cmd.execute(); } for (ResourceGroup *g : qAsConst(removegroups)) { debugPlanShared<<"Delete group:"<id(); RemoveResourceGroupCmd cmd(&project, g); cmd.execute(); } // insert new objects Q_ASSERT(project.childNodeIterator().isEmpty()); InsertProjectCmd cmd(project, m_project, 0); cmd.execute(); return true; } void MainDocument::insertViewListItem(View */*view*/, const ViewListItem *item, const ViewListItem *parent, int index) { // FIXME callers should take care that they now get a signal even if originating from themselves emit viewListItemAdded(item, parent, index); setModified(true); m_viewlistModified = true; } void MainDocument::removeViewListItem(View */*view*/, const ViewListItem *item) { // FIXME callers should take care that they now get a signal even if originating from themselves emit viewListItemRemoved(item); setModified(true); m_viewlistModified = true; } bool MainDocument::isLoading() const { return m_isLoading || KoDocument::isLoading(); } void MainDocument::setModified(bool mod) { debugPlan<name().isEmpty()) { setUrl(QUrl(m_project->name() + ".plan")); } if (m_project->scheduleManagers().isEmpty()) { ScheduleManager *sm = m_project->createScheduleManager(); sm->setAllowOverbooking(false); sm->setSchedulingMode(ScheduleManager::AutoMode); } Calendar *week = nullptr; if (KPlatoSettings::generateWeek()) { bool always = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::Always; bool ifnone = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::NoneExists; if (always || (ifnone && m_project->calendarCount() == 0)) { // create a calendar week = new Calendar(i18nc("Base calendar name", "Base")); m_project->addCalendar(week); CalendarDay vd(CalendarDay::NonWorking); for (int i = Qt::Monday; i <= Qt::Sunday; ++i) { if (m_config.isWorkingday(i)) { CalendarDay wd(CalendarDay::Working); TimeInterval ti(m_config.dayStartTime(i), m_config.dayLength(i)); wd.addInterval(ti); week->setWeekday(i, wd); } else { week->setWeekday(i, vd); } } + m_project->setDefaultCalendar(week); } } #ifdef HAVE_KHOLIDAYS if (KPlatoSettings::generateHolidays()) { bool inweek = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::InWeekCalendar; bool subcalendar = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSubCalendar; bool separate = week == 0 || KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSeparateCalendar; Calendar *holiday = nullptr; if (inweek) { holiday = week; week->setDefault(true); debugPlan<<"in week"; } else if (subcalendar) { holiday = new Calendar(i18n("Holidays")); m_project->addCalendar(holiday, week); - holiday->setDefault(true); debugPlan<<"subcalendar"; } else if (separate) { holiday = new Calendar(i18n("Holidays")); m_project->addCalendar(holiday); - holiday->setDefault(true); debugPlan<<"separate"; } else { Q_ASSERT(false); // something wrong } debugPlan<setHolidayRegion(KPlatoSettings::region()); + m_project->setDefaultCalendar(holiday); } -#else - week->setDefault(true); #endif } // creates a "new" project from current project (new ids etc) void MainDocument::createNewProject() { setEmpty(); clearUndoHistory(); setModified(false); resetURL(); KoDocumentInfo *info = documentInfo(); info->resetMetaData(); info->setProperty("title", ""); setTitleModified(); m_project->generateUniqueNodeIds(); Duration dur = m_project->constraintEndTime() - m_project->constraintStartTime(); m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime)); m_project->setConstraintEndTime(m_project->constraintStartTime() + dur); while (m_project->numScheduleManagers() > 0) { foreach (ScheduleManager *sm, m_project->allScheduleManagers()) { if (sm->childCount() > 0) { continue; } if (sm->expected()) { sm->expected()->setDeleted(true); sm->setExpected(0); } m_project->takeScheduleManager(sm); delete sm; } } foreach (Schedule *s, m_project->schedules()) { m_project->takeSchedule(s); delete s; } foreach (Node *n, m_project->allNodes()) { foreach (Schedule *s, n->schedules()) { n->takeSchedule(s); delete s; } } foreach (Resource *r, m_project->resourceList()) { foreach (Schedule *s, r->schedules()) { r->takeSchedule(s); delete s; } } } void MainDocument::setIsTaskModule(bool value) { m_isTaskModule = value; } bool MainDocument::isTaskModule() const { return m_isTaskModule; } } //KPlato namespace