| 3162 | } |
| 3163 | |
| 3164 | void MainWindow::restoreDataProcessors(const QDomElement& root) { |
| 3165 | // Reconcile the live filter set to this snapshot: drop ALL current filters first, |
| 3166 | // then recreate the snapshot's set. This makes restore idempotent for undo/redo (no |
| 3167 | // duplicate or output-name-colliding filters) and correct for a layout load onto an |
| 3168 | // existing session. A snapshot with NO <data_processors> still falls through to clear |
| 3169 | // every filter; the rebuild at the end MUST run on both branches so the now-retired |
| 3170 | // outputs leave the catalog. |
| 3171 | auto& service = session_->sessionManager().dataProcessorService(); |
| 3172 | service.clearAllFilters(); |
| 3173 | |
| 3174 | const QDomElement element = root.firstChildElement(QStringLiteral("data_processors")); |
| 3175 | const auto datasets = session_->catalogModel().datasets(); |
| 3176 | // A null <data_processors> yields a null firstChildElement, so this loop runs zero |
| 3177 | // times — the clear above is then the whole effect. |
| 3178 | for (QDomElement processor = element.firstChildElement(QStringLiteral("processor")); !processor.isNull(); |
| 3179 | processor = processor.nextSiblingElement(QStringLiteral("processor"))) { |
| 3180 | const QString input_topic = processor.attribute(QStringLiteral("input_topic")); |
| 3181 | const QString input_field = processor.attribute(QStringLiteral("input_field")); |
| 3182 | // Resolve the input against whichever loaded dataset holds it (first match in |
| 3183 | // load order), so a multi-file layout restores each filter against its source. |
| 3184 | std::optional<CurveDescriptor> input_desc; |
| 3185 | for (const auto& [dataset_id, dataset_name] : datasets) { |
| 3186 | (void)dataset_name; |
| 3187 | if (auto descriptor = session_->catalogModel().descriptorForPath(dataset_id, input_topic, input_field)) { |
| 3188 | input_desc = std::move(descriptor); |
| 3189 | break; |
| 3190 | } |
| 3191 | } |
| 3192 | if (!input_desc.has_value()) { |
| 3193 | // The filter's source signal isn't in any loaded dataset -> the filter is |
| 3194 | // dropped. Tell the user which one, mirroring the unknown/apply-failed cases |
| 3195 | // below (otherwise a derived series silently vanishes on layout load). |
| 3196 | emitDiagnostic( |
| 3197 | DiagnosticLevel::kWarning, "Layout", "processor-input-missing", |
| 3198 | tr("Layout filter on '%1/%2' has no matching data; skipping.").arg(input_topic, input_field)); |
| 3199 | continue; |
| 3200 | } |
| 3201 | const std::string id = processor.attribute(QStringLiteral("processor_id")).toStdString(); |
| 3202 | // Params are the <processor>'s OWN direct CDATA — directCdataText ignores the |
| 3203 | // <source_fallback> child (QDomElement::text() would recurse and merge them). |
| 3204 | const QString params = layout_xml::directCdataText(processor); |
| 3205 | const QString source_fallback = processor.firstChildElement(QStringLiteral("source_fallback")).text(); |
| 3206 | // Resolve order: live catalogue → embedded source → transitional C++ builtin. |
| 3207 | std::unique_ptr<proc::DataProcessor> built = service.makeRestoredProcessor( |
| 3208 | id, params.isEmpty() ? std::string("{}") : params.toStdString(), source_fallback.toStdString()); |
| 3209 | if (!built) { |
| 3210 | emitDiagnostic( |
| 3211 | DiagnosticLevel::kWarning, "Layout", "processor-unknown", |
| 3212 | tr("Layout filter '%1' is unknown to this PlotJuggler; skipping.").arg(QString::fromStdString(id))); |
| 3213 | continue; |
| 3214 | } |
| 3215 | const auto applied = service.applyFilter( |
| 3216 | input_desc->topic_id, input_desc->dataset_id, std::move(built), |
| 3217 | processor.attribute(QStringLiteral("output_name")).toStdString(), input_desc->column_index); |
| 3218 | if (!applied.has_value()) { |
| 3219 | emitDiagnostic( |
| 3220 | DiagnosticLevel::kWarning, "Layout", "processor-apply-failed", |
| 3221 | tr("Could not restore filter: %1").arg(QString::fromStdString(applied.error()))); |
nothing calls this directly
no test coverage detected