File indexing completed on 2024-04-28 05:49:29
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org> 0003 SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org> 0004 SPDX-FileCopyrightText: 2007 Flavio Castelli <flavio.castelli@gmail.com> 0005 0006 SPDX-License-Identifier: LGPL-2.0-only 0007 */ 0008 0009 #include "katedocmanager.h" 0010 0011 #include "kateapp.h" 0012 #include "katemainwindow.h" 0013 #include "katesavemodifieddialog.h" 0014 #include "kateviewmanager.h" 0015 0016 #include <kcoreaddons_version.h> 0017 #include <ktexteditor/editor.h> 0018 #include <ktexteditor/view.h> 0019 0020 #include <KConfigGroup> 0021 #include <KLocalizedString> 0022 #include <KMessageBox> 0023 #include <KNetworkMounts> 0024 #include <KSharedConfig> 0025 #include <kwidgetsaddons_version.h> 0026 0027 #include <QFileDialog> 0028 #include <QProgressDialog> 0029 0030 KateDocManager::KateDocManager(QObject *parent) 0031 : QObject(parent) 0032 , m_metaInfos(KateApp::isKate() ? QStringLiteral("katemetainfos") : QStringLiteral("kwritemetainfos"), KConfig::NoGlobals) 0033 , m_saveMetaInfos(true) 0034 , m_daysMetaInfos(0) 0035 { 0036 // set our application wrapper 0037 KTextEditor::Editor::instance()->setApplication(KateApp::self()->wrapper()); 0038 } 0039 0040 KateDocManager::~KateDocManager() 0041 { 0042 // write metainfos? 0043 if (m_saveMetaInfos) { 0044 // saving meta-infos when file is saved is not enough, we need to do it once more at the end 0045 saveMetaInfos(m_docList); 0046 0047 // purge saved filesessions 0048 if (m_daysMetaInfos > 0) { 0049 const QStringList groups = m_metaInfos.groupList(); 0050 QDateTime def(QDate(1970, 1, 1).startOfDay()); 0051 0052 for (const auto &group : groups) { 0053 QDateTime last = m_metaInfos.group(group).readEntry("Time", def); 0054 if (last.daysTo(QDateTime::currentDateTimeUtc()) > m_daysMetaInfos) { 0055 m_metaInfos.deleteGroup(group); 0056 } 0057 } 0058 } 0059 } 0060 } 0061 0062 static QUrl normalizeUrl(const QUrl &url) 0063 { 0064 // Resolve symbolic links for local files 0065 if (url.isLocalFile() && !KNetworkMounts::self()->isOptionEnabledForPath(url.toLocalFile(), KNetworkMounts::StrongSideEffectsOptimizations)) { 0066 QString normalizedUrl = QFileInfo(url.toLocalFile()).canonicalFilePath(); 0067 if (!normalizedUrl.isEmpty()) { 0068 return QUrl::fromLocalFile(normalizedUrl); 0069 } 0070 } 0071 0072 // else: cleanup only the .. stuff 0073 return url.adjusted(QUrl::NormalizePathSegments); 0074 } 0075 0076 static QUrl absoluteUrl(const QUrl &url) 0077 { 0078 // Get absolute path if local file 0079 if (url.isLocalFile()) { 0080 return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).absoluteFilePath()); 0081 } 0082 0083 // else: cleanup only the .. stuff 0084 return url.adjusted(QUrl::NormalizePathSegments); 0085 } 0086 0087 void KateDocManager::slotUrlChanged(const QUrl &newUrl) 0088 { 0089 KTextEditor::Document *doc = qobject_cast<KTextEditor::Document *>(sender()); 0090 if (doc) { 0091 m_docInfos.at(doc).normalizedUrl = normalizeUrl(newUrl); 0092 } 0093 } 0094 0095 KTextEditor::Document *KateDocManager::createDoc(const KateDocumentInfo &docInfo) 0096 { 0097 KTextEditor::Document *doc = KTextEditor::Editor::instance()->createDocument(this); 0098 0099 // turn off the editorpart's own modification dialog, we have our own one, too! 0100 const KConfigGroup generalGroup(KSharedConfig::openConfig(), QStringLiteral("General")); 0101 bool ownModNotification = generalGroup.readEntry("Modified Notification", false); 0102 doc->setModifiedOnDiskWarning(!ownModNotification); 0103 0104 m_docList.push_back(doc); 0105 m_docInfos.emplace(doc, docInfo); 0106 0107 // connect internal signals... 0108 connect(doc, &KTextEditor::Document::modifiedChanged, this, &KateDocManager::slotModChanged1); 0109 connect(doc, &KTextEditor::Document::modifiedOnDisk, this, &KateDocManager::slotModifiedOnDisc); 0110 connect(doc, &KTextEditor::Document::documentUrlChanged, this, [this](KTextEditor::Document *doc) { 0111 slotUrlChanged(doc->url()); 0112 }); 0113 connect(doc, &KParts::ReadOnlyPart::urlChanged, this, &KateDocManager::slotUrlChanged); 0114 0115 // we have a new document, show it the world 0116 Q_EMIT documentCreated(doc); 0117 0118 // return our new document 0119 return doc; 0120 } 0121 0122 KateDocumentInfo *KateDocManager::documentInfo(KTextEditor::Document *doc) 0123 { 0124 auto it = m_docInfos.find(doc); 0125 if (it != m_docInfos.end()) { 0126 return &it->second; 0127 } 0128 return nullptr; 0129 } 0130 0131 KTextEditor::Document *KateDocManager::findDocument(const QUrl &url) const 0132 { 0133 auto it = std::find_if(m_docInfos.begin(), m_docInfos.end(), [u = normalizeUrl(url)](const auto &p) { 0134 return p.second.normalizedUrl == u; 0135 }); 0136 0137 return it == m_docInfos.end() ? nullptr : it->first; 0138 } 0139 0140 std::vector<KTextEditor::Document *> KateDocManager::openUrls(const QList<QUrl> &urls, const QString &encoding, const KateDocumentInfo &docInfo) 0141 { 0142 std::vector<KTextEditor::Document *> docs; 0143 docs.reserve(urls.size()); 0144 for (const QUrl &url : urls) { 0145 docs.push_back(openUrl(url, encoding, docInfo)); 0146 } 0147 return docs; 0148 } 0149 0150 KTextEditor::Document *KateDocManager::openUrl(const QUrl &url, const QString &encoding, const KateDocumentInfo &docInfo) 0151 { 0152 // We want to work on absolute urls 0153 const QUrl u(absoluteUrl(url)); 0154 0155 // try to find already open document 0156 if (!u.isEmpty()) { 0157 if (auto doc = findDocument(u)) { 0158 return doc; 0159 } 0160 } 0161 0162 // else: create new document 0163 auto doc = createDoc(docInfo); 0164 if (!encoding.isEmpty()) { 0165 doc->setEncoding(encoding); 0166 } 0167 if (!u.isEmpty()) { 0168 doc->openUrl(u); 0169 loadMetaInfos(doc, u); 0170 } 0171 return doc; 0172 } 0173 0174 bool KateDocManager::closeDocuments(const QList<KTextEditor::Document *> documents, bool closeUrl) 0175 { 0176 if (documents.empty()) { 0177 return false; 0178 } 0179 0180 saveMetaInfos(documents); 0181 0182 Q_EMIT aboutToDeleteDocuments(QList<KTextEditor::Document *>{documents.begin(), documents.end()}); 0183 0184 int last = 0; 0185 bool success = true; 0186 for (KTextEditor::Document *doc : documents) { 0187 if (closeUrl && !doc->closeUrl()) { 0188 success = false; // get out on first error 0189 break; 0190 } 0191 0192 // document will be deleted, soon 0193 Q_EMIT documentWillBeDeleted(doc); 0194 0195 // really delete the document and its infos 0196 disconnect(doc, &KParts::ReadOnlyPart::urlChanged, this, &KateDocManager::slotUrlChanged); 0197 m_docInfos.erase(doc); 0198 delete m_docList.takeAt(m_docList.indexOf(doc)); 0199 0200 // document is gone, emit our signals 0201 Q_EMIT documentDeleted(doc); 0202 0203 last++; 0204 } 0205 0206 Q_EMIT documentsDeleted(QList<KTextEditor::Document *>{documents.begin() + last, documents.end()}); 0207 0208 return success; 0209 } 0210 0211 bool KateDocManager::closeDocument(KTextEditor::Document *doc, bool closeUrl) 0212 { 0213 if (!doc) { 0214 return false; 0215 } 0216 0217 return closeDocuments({doc}, closeUrl); 0218 } 0219 0220 bool KateDocManager::closeDocumentList(const QList<KTextEditor::Document *> &documents, KateMainWindow *window) 0221 { 0222 std::vector<KTextEditor::Document *> modifiedDocuments; 0223 for (KTextEditor::Document *document : documents) { 0224 if (document->isModified()) { 0225 modifiedDocuments.push_back(document); 0226 } 0227 } 0228 0229 if (!modifiedDocuments.empty() && !KateSaveModifiedDialog::queryClose(window, modifiedDocuments)) { 0230 return false; 0231 } 0232 0233 return closeDocuments(documents, false); // Do not show save/discard dialog 0234 } 0235 0236 bool KateDocManager::closeAllDocuments(bool closeUrl) 0237 { 0238 /** 0239 * just close all documents 0240 */ 0241 return closeDocuments(m_docList, closeUrl); 0242 } 0243 0244 bool KateDocManager::closeOtherDocuments(KTextEditor::Document *doc) 0245 { 0246 /** 0247 * close all documents beside the passed one 0248 */ 0249 QList<KTextEditor::Document *> documents; 0250 documents.reserve(m_docList.size() - 1); 0251 for (auto document : qAsConst(m_docList)) { 0252 if (document != doc) { 0253 documents.push_back(document); 0254 } 0255 } 0256 0257 return closeDocuments(documents); 0258 } 0259 0260 /** 0261 * Find all modified documents. 0262 * @return Return the list of all modified documents. 0263 */ 0264 std::vector<KTextEditor::Document *> KateDocManager::modifiedDocumentList() 0265 { 0266 std::vector<KTextEditor::Document *> modified; 0267 std::copy_if(m_docList.begin(), m_docList.end(), std::back_inserter(modified), [](KTextEditor::Document *doc) { 0268 return doc->isModified(); 0269 }); 0270 return modified; 0271 } 0272 0273 bool KateDocManager::queryCloseDocuments(KateMainWindow *w) 0274 { 0275 const auto docCount = m_docList.size(); 0276 for (KTextEditor::Document *doc : qAsConst(m_docList)) { 0277 if (doc->url().isEmpty() && doc->isModified()) { 0278 int msgres = KMessageBox::warningTwoActionsCancel(w, 0279 i18n("<p>The document '%1' has been modified, but not saved.</p>" 0280 "<p>Do you want to save your changes or discard them?</p>", 0281 doc->documentName()), 0282 i18n("Close Document"), 0283 KStandardGuiItem::save(), 0284 KStandardGuiItem::discard()); 0285 0286 if (msgres == KMessageBox::Cancel) { 0287 return false; 0288 } 0289 0290 if (msgres == KMessageBox::PrimaryAction) { 0291 const QUrl url = QFileDialog::getSaveFileUrl(w, i18n("Save As")); 0292 if (!url.isEmpty()) { 0293 if (!doc->saveAs(url)) { 0294 return false; 0295 } 0296 } else { 0297 return false; 0298 } 0299 } 0300 } else { 0301 if (!doc->queryClose()) { 0302 return false; 0303 } 0304 } 0305 } 0306 0307 // document count changed while queryClose, abort and notify user 0308 if (m_docList.size() > docCount) { 0309 KMessageBox::information(w, i18n("New file opened while trying to close Kate, closing aborted."), i18n("Closing Aborted")); 0310 return false; 0311 } 0312 0313 return true; 0314 } 0315 0316 void KateDocManager::saveAll() 0317 { 0318 for (KTextEditor::Document *doc : qAsConst(m_docList)) { 0319 if (doc->isModified()) { 0320 doc->documentSave(); 0321 } 0322 } 0323 } 0324 0325 void KateDocManager::saveSelected(const QList<KTextEditor::Document *> &docList) 0326 { 0327 for (KTextEditor::Document *doc : docList) { 0328 if (doc->isModified()) { 0329 doc->documentSave(); 0330 } 0331 } 0332 } 0333 0334 void KateDocManager::reloadAll() 0335 { 0336 // reload all docs that are NOT modified on disk 0337 for (KTextEditor::Document *doc : qAsConst(m_docList)) { 0338 if (!documentInfo(doc)->modifiedOnDisc) { 0339 doc->documentReload(); 0340 } 0341 } 0342 0343 // take care of all documents that ARE modified on disk 0344 KateApp::self()->activeKateMainWindow()->showModOnDiskPrompt(KateMainWindow::PromptAll); 0345 } 0346 0347 void KateDocManager::closeOrphaned() 0348 { 0349 QList<KTextEditor::Document *> documents; 0350 0351 for (KTextEditor::Document *doc : qAsConst(m_docList)) { 0352 KateDocumentInfo *info = documentInfo(doc); 0353 if (info && !info->openSuccess) { 0354 documents.push_back(doc); 0355 } 0356 } 0357 0358 closeDocuments(documents); 0359 } 0360 0361 void KateDocManager::saveDocumentList(KConfig *config) 0362 { 0363 KConfigGroup openDocGroup(config, QStringLiteral("Open Documents")); 0364 0365 openDocGroup.writeEntry("Count", (int)m_docList.size()); 0366 0367 int i = 0; 0368 for (KTextEditor::Document *doc : qAsConst(m_docList)) { 0369 const QString entryName = QStringLiteral("Document %1").arg(i); 0370 KConfigGroup cg(config, entryName); 0371 doc->writeSessionConfig(cg); 0372 0373 i++; 0374 } 0375 } 0376 0377 void KateDocManager::restoreDocumentList(KConfig *config) 0378 { 0379 KConfigGroup openDocGroup(config, QStringLiteral("Open Documents")); 0380 unsigned int count = openDocGroup.readEntry("Count", 0); 0381 0382 if (count == 0) { 0383 return; 0384 } 0385 0386 QProgressDialog progress; 0387 progress.setWindowTitle(i18n("Starting Up")); 0388 progress.setLabelText(i18n("Reopening files from the last session...")); 0389 progress.setModal(true); 0390 progress.setCancelButton(nullptr); 0391 progress.setRange(0, count); 0392 0393 for (unsigned int i = 0; i < count; i++) { 0394 KConfigGroup cg(config, QStringLiteral("Document %1").arg(i)); 0395 KTextEditor::Document *doc = createDoc(); 0396 0397 connect(doc, SIGNAL(completed()), this, SLOT(documentOpened())); 0398 connect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened); 0399 0400 doc->readSessionConfig(cg); 0401 0402 KateApp::self()->stashManager()->popDocument(doc, cg); 0403 0404 progress.setValue(i); 0405 } 0406 } 0407 0408 void KateDocManager::slotModifiedOnDisc(KTextEditor::Document *doc, bool b, KTextEditor::Document::ModifiedOnDiskReason reason) 0409 { 0410 auto it = m_docInfos.find(doc); 0411 if (it != m_docInfos.end()) { 0412 it->second.modifiedOnDisc = b; 0413 it->second.modifiedOnDiscReason = reason; 0414 slotModChanged1(doc); 0415 } 0416 } 0417 0418 /** 0419 * Load file's meta-information if the checksum didn't change since last time. 0420 */ 0421 bool KateDocManager::loadMetaInfos(KTextEditor::Document *doc, const QUrl &url) 0422 { 0423 if (!m_saveMetaInfos) { 0424 return false; 0425 } 0426 0427 if (!m_metaInfos.hasGroup(url.toDisplayString())) { 0428 return false; 0429 } 0430 0431 const QByteArray checksum = doc->checksum().toHex(); 0432 bool ok = true; 0433 if (!checksum.isEmpty()) { 0434 KConfigGroup urlGroup(&m_metaInfos, url.toDisplayString()); 0435 const QString old_checksum = urlGroup.readEntry("Checksum"); 0436 0437 if (QString::fromLatin1(checksum) == old_checksum) { 0438 QSet<QString> flags; 0439 if (documentInfo(doc)->openedByUser) { 0440 flags << QStringLiteral("SkipEncoding"); 0441 } 0442 flags << QStringLiteral("SkipUrl"); 0443 doc->readSessionConfig(urlGroup, flags); 0444 } else { 0445 urlGroup.deleteGroup(); 0446 ok = false; 0447 } 0448 } 0449 0450 return ok && doc->url() == url; 0451 } 0452 0453 /** 0454 * Save file's meta-information if doc is in 'unmodified' state 0455 */ 0456 0457 void KateDocManager::saveMetaInfos(const QList<KTextEditor::Document *> &documents) 0458 { 0459 /** 0460 * skip work if no meta infos wanted 0461 */ 0462 if (!m_saveMetaInfos) { 0463 return; 0464 } 0465 0466 const QSet<QString> flags{QStringLiteral("SkipUrl")}; 0467 0468 /** 0469 * store meta info for all non-modified documents which have some checksum 0470 */ 0471 const QDateTime now = QDateTime::currentDateTimeUtc(); 0472 for (KTextEditor::Document *doc : documents) { 0473 /** 0474 * skip modified docs 0475 */ 0476 if (doc->isModified()) { 0477 continue; 0478 } 0479 0480 const QByteArray checksum = doc->checksum().toHex(); 0481 if (!checksum.isEmpty()) { 0482 /** 0483 * write the group with checksum and time 0484 */ 0485 const QString url = doc->url().toString(); 0486 KConfigGroup urlGroup(&m_metaInfos, url); 0487 0488 /** 0489 * write document session config 0490 */ 0491 doc->writeSessionConfig(urlGroup, flags); 0492 if (!urlGroup.keyList().isEmpty()) { 0493 urlGroup.writeEntry("URL", url); 0494 urlGroup.writeEntry("Checksum", QString::fromLatin1(checksum)); 0495 urlGroup.writeEntry("Time", now); 0496 } else { 0497 urlGroup.deleteGroup(); 0498 } 0499 } 0500 } 0501 } 0502 0503 void KateDocManager::slotModChanged(KTextEditor::Document *doc) 0504 { 0505 saveMetaInfos({doc}); 0506 } 0507 0508 void KateDocManager::slotModChanged1(KTextEditor::Document *doc) 0509 { 0510 // clang-format off 0511 QMetaObject::invokeMethod(KateApp::self()->activeKateMainWindow(), "queueModifiedOnDisc", Qt::QueuedConnection, Q_ARG(KTextEditor::Document*,doc)); 0512 // clang-format on 0513 0514 if (doc->isModified()) { 0515 KateDocumentInfo *info = documentInfo(doc); 0516 if (info) { 0517 info->wasDocumentEverModified = true; 0518 } 0519 } 0520 } 0521 0522 void KateDocManager::documentOpened() 0523 { 0524 KTextEditor::Document *doc = qobject_cast<KTextEditor::Document *>(sender()); 0525 if (!doc) { 0526 return; // should never happen, but who knows 0527 } 0528 disconnect(doc, SIGNAL(completed()), this, SLOT(documentOpened())); 0529 disconnect(doc, &KParts::ReadOnlyPart::canceled, this, &KateDocManager::documentOpened); 0530 0531 // Only set "no success" when doc is empty to avoid close of files 0532 // with other trouble when do closeOrphaned() 0533 if (doc->openingError() && doc->isEmpty()) { 0534 KateDocumentInfo *info = documentInfo(doc); 0535 if (info) { 0536 info->openSuccess = false; 0537 } 0538 } 0539 } 0540 0541 #include "moc_katedocmanager.cpp"