| 3453 | } |
| 3454 | |
| 3455 | QDomElement MainWindow::appendDataSourceElement(QDomDocument& doc, const QDir& layout_dir) const { |
| 3456 | const auto& sources = session_->sessionManager().loadedSources(); |
| 3457 | // Do not save a data-source reference after the catalog was cleared. |
| 3458 | if (sources.empty() || session_->catalogModel().isEmpty()) { |
| 3459 | return QDomElement(); |
| 3460 | } |
| 3461 | |
| 3462 | // Persist only sources that still back a live dataset. A dataset whose curves |
| 3463 | // were all removed (Remove Dataset, or trashing every topic) drops out of |
| 3464 | // datasets(), so its file is neither serialized here nor resurrected on the |
| 3465 | // next reload — while loaded_sources_ itself stays intact for the quick-reload |
| 3466 | // button (which keys off lastLoadedSource, not this list). FileLoader owns the |
| 3467 | // DatasetId->path link the engine's basename-only DatasetInfo can't provide. |
| 3468 | QSet<QString> live_paths; |
| 3469 | QHash<QString, DatasetId> dataset_for_path; // reverse of sourcePathForDataset, for offset lookup |
| 3470 | for (const auto& [id, name] : session_->catalogModel().datasets()) { |
| 3471 | (void)name; |
| 3472 | if (const QString src_path = file_loader_->sourcePathForDataset(id); !src_path.isEmpty()) { |
| 3473 | live_paths.insert(src_path); |
| 3474 | dataset_for_path.insert(src_path, id); |
| 3475 | } |
| 3476 | } |
| 3477 | |
| 3478 | // Source Timeline arrangement: the bar's top-to-bottom slot, persisted per |
| 3479 | // file so the vertical order round-trips (re-bound by path on reload). Build |
| 3480 | // dataset -> slot once; absent => fall back to load order on restore. |
| 3481 | QHash<DatasetId, int> timeline_order; |
| 3482 | if (source_timeline_controller_ != nullptr) { |
| 3483 | const std::vector<DatasetId> order = source_timeline_controller_->currentTrackOrder(); |
| 3484 | for (int i = 0; i < static_cast<int>(order.size()); ++i) { |
| 3485 | timeline_order.insert(order[static_cast<std::size_t>(i)], i); |
| 3486 | } |
| 3487 | } |
| 3488 | |
| 3489 | QDomElement wrapper = doc.createElement(QStringLiteral("previouslyLoaded_Datafiles")); |
| 3490 | |
| 3491 | // One <fileInfo> per loaded file, in load order, so a multi-file session |
| 3492 | // round-trips. Old PJ4 readers that only read the first child degrade to the |
| 3493 | // first file; new readers restore them all. |
| 3494 | for (const auto& src : sources) { |
| 3495 | if (!live_paths.contains(src.path)) { |
| 3496 | continue; // dataset removed since load; don't resurrect it on reload |
| 3497 | } |
| 3498 | QDomElement file_info = doc.createElement(QStringLiteral("fileInfo")); |
| 3499 | |
| 3500 | const QFileInfo info(src.path); |
| 3501 | const QString abs = info.absoluteFilePath(); |
| 3502 | const QString rel = layout_dir.relativeFilePath(abs); |
| 3503 | // Prefer the relative form when the data lives at or beneath the layout |
| 3504 | // dir; fall back to absolute when it escapes. This diverges from PJ3, |
| 3505 | // which always stores relative — PJ4 avoids brittle ../.. paths so that |
| 3506 | // moving a layout file doesn't silently break the data reference. |
| 3507 | // A relative path is a "subpath" only when Qt's relativeFilePath did |
| 3508 | // NOT emit a "../" prefix or the literal ".." path. The earlier check |
| 3509 | // (`!rel.startsWith("..")`) would misclassify legitimate filenames |
| 3510 | // like "..foo" or "..bar/data.csv" as escaping the dir. |
| 3511 | const bool is_subpath = rel != QStringLiteral("..") && !rel.startsWith(QStringLiteral("../")); |
| 3512 | file_info.setAttribute(QStringLiteral("filename"), is_subpath ? rel : abs); |
nothing calls this directly
no test coverage detected