wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
GraphicElementSerializer.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
9
11
12#include <cmath>
13
14#include "App/Core/Common.h"
19#include "App/IO/VersionInfo.h"
20#include "App/Wiring/Port.h"
21
22namespace {
23
31constexpr quint32 kMaxPortsPerElement = 1024;
32
33} // namespace
34
35// readPortList() and removePortFromMap() are static members (not file-local helpers) so their
36// load-error PANDACEPTION throws extract under the GraphicElementSerializer tr context.
37
38QList<QMap<QString, QVariant>> GraphicElementSerializer::readPortList(QDataStream &stream, const char *label)
39{
40 const QString labelStr = QString::fromUtf8(label);
41 quint32 count;
42 stream >> count;
43 if (stream.status() != QDataStream::Ok) {
44 throw PANDACEPTION("Stream error reading %1 count at position %2",
45 labelStr, QString::number(stream.device()->pos()));
46 }
47 if (count > kMaxPortsPerElement) {
48 throw PANDACEPTION("Refusing to read %1 with implausible count %2 (max %3)",
49 labelStr,
50 QString::number(count),
51 QString::number(kMaxPortsPerElement));
52 }
53 QList<QMap<QString, QVariant>> result;
54 result.reserve(static_cast<int>(count));
55 for (quint32 i = 0; i < count; ++i) {
56 QMap<QString, QVariant> entry = Serialization::readBoundedMetadata(stream);
57 if (stream.status() != QDataStream::Ok) {
58 throw PANDACEPTION("Stream error reading %1 entry %2 at position %3",
59 labelStr,
60 QString::number(i),
61 QString::number(stream.device()->pos()));
62 }
63 result.append(std::move(entry));
64 }
65 return result;
66}
67
68void GraphicElementSerializer::removePortFromMap(Port *deletedPort, QHash<quint64, Port *> &portMap)
69{
70 for (auto it = portMap.begin(); it != portMap.end();) {
71 if (it.value() == deletedPort) {
72 it = portMap.erase(it);
73 } else {
74 ++it;
75 }
76 }
77}
78
79// ========== GraphicElement entry points (delegate to the serializer) ==========
80
81void GraphicElement::save(QDataStream &stream, SerializationOptions options) const
82{
83 GraphicElementSerializer::save(*this, stream, options);
84}
85
86void GraphicElement::load(QDataStream &stream, SerializationContext &context)
87{
88 GraphicElementSerializer::load(*this, stream, context);
89}
90
91// ========== save / load ==========
92
93void GraphicElementSerializer::save(const GraphicElement &element, QDataStream &stream, SerializationOptions options)
94{
95 qCDebug(four) << "Saving element. Type: " << element.objectName();
96
97 // PortableFile streams elide everything the loader either derives or
98 // reconstructs from constructor defaults (fresh elements only — see
99 // loadNewFormat's contains() guards). InMemorySnapshot streams write every
100 // field unconditionally: they are loaded back into EXISTING live elements
101 // (undo, rollback), where a missing key would silently keep the current
102 // value instead of restoring the snapshotted one.
103 const bool slim = (options.purpose == SerializationPurpose::PortableFile);
104
105 QMap<QString, QVariant> map;
106 map.insert("pos", element.pos());
107 if (slim) {
108 // Ports carry no serialId in slim streams; the loader re-derives them
109 // from this id + each port's list position (see Port::makeSerialId()).
110 map.insert("id", element.id());
111 }
112 if (!slim || element.rotation() != 0.0) {
113 map.insert("rotation", element.rotation());
114 }
115 if (!slim || !element.label().isEmpty()) {
116 map.insert("label", element.label());
117 }
118 // min/max port sizes are class metadata, not per-instance state.
119 // No longer written; old files that contain these keys are harmlessly
120 // ignored on load (the QMap is read as a whole, unused keys are skipped).
121 if (!slim || !element.trigger().isEmpty()) {
122 map.insert("trigger", element.trigger());
123 }
124 if (element.isFlippedX()) { map.insert("flippedX", true); }
125 if (element.isFlippedY()) { map.insert("flippedY", true); }
126 stream << map;
127
128 // -------------------------------------------
129
130 // Port entries. Slim streams write empty maps for non-IC ports: the
131 // serialId is derived from the element id above, and the loader ignores
132 // non-IC port names anyway (updatePortsProperties() recomputes them at the
133 // end of every load; no UI path renames ports). The list itself still
134 // carries the port COUNT, which the loader needs to restore port sizes.
135 const bool keepNames = !slim || (element.elementType() == ElementType::IC);
136
137 QList<QMap<QString, QVariant>> inputMap;
138
139 for (int i = 0; i < element.m_ports.inputs().size(); ++i) {
140 auto *port = element.m_ports.inputs().at(i);
141 QMap<QString, QVariant> tempMap;
142 if (!slim) {
143 // Note: We calculate but don't modify port state (no side effects in save())
144 quint64 serialId = Port::makeSerialId(static_cast<quint64>(element.id()), port->globalIndex());
145 tempMap.insert("serialId", serialId);
146 }
147 if (keepNames) {
148 tempMap.insert("name", port->name());
149 }
150
151 inputMap << tempMap;
152 }
153
154 stream << inputMap;
155
156 // -------------------------------------------
157
158 QList<QMap<QString, QVariant>> outputMap;
159
160 for (int i = 0; i < element.m_ports.outputs().size(); ++i) {
161 auto *port = element.m_ports.outputs().at(i);
162 QMap<QString, QVariant> tempMap;
163 if (!slim) {
164 // Note: We calculate but don't modify port state (no side effects in save())
165 quint64 serialId = Port::makeSerialId(static_cast<quint64>(element.id()), port->globalIndex());
166 tempMap.insert("serialId", serialId);
167 }
168 if (keepNames) {
169 tempMap.insert("name", port->name());
170 }
171
172 outputMap << tempMap;
173 }
174
175 stream << outputMap;
176
177 // -------------------------------------------
178
179 // Appearance entries. Slim streams write resource paths (":/...") as empty
180 // maps: forReading() discards them on every PortableFile load anyway (a
181 // stale saved resource identifier must not clobber the element's current
182 // compiled-in default). Kept per-slot — NOT gated on the element-level
183 // usingDefaultAppearance() flag, which is maintained manually by scattered
184 // callers and a stale-true value would silently drop a live custom skin;
185 // the resource-vs-not test matches forReading()'s semantics by construction.
186 // Empty maps (rather than omitted entries) preserve positional slot
187 // alignment when custom and default slots are mixed; the all-default case
188 // collapses to a list of empty maps, or the loop below to an empty list.
189 QList<QMap<QString, QVariant>> appearancesMap;
190 bool anyCustom = false;
191
192 for (const auto &appearance : element.m_appearance.alternativeAppearances()) {
193 QMap<QString, QVariant> tempMap;
194 if (!slim || !appearance.startsWith(":/")) {
195 tempMap.insert("skinName", ExternalFilePath::forWriting(appearance, options.purpose));
196 anyCustom = true;
197 }
198 appearancesMap << tempMap;
199 }
200
201 if (slim && !anyCustom) {
202 appearancesMap.clear();
203 }
204
205 stream << appearancesMap;
206
207 // -------------------------------------------
208
209 qCDebug(four) << "Finished saving element.";
210}
211
212void GraphicElementSerializer::load(GraphicElement &element, QDataStream &stream, SerializationContext &context)
213{
214 qCDebug(four) << "Loading element. Type: " << element.objectName();
215
216 // Files before 4.1 used a flat sequential binary format; 4.1+ use a keyed QMap
217 // format that tolerates fields being added or reordered in future versions.
218 (!VersionInfo::hasQMapFormat(context.version)) ? loadOldFormat(element, stream, context) : loadNewFormat(element, stream, context);
219
220 qCDebug(four) << "Updating port positions.";
221 element.updatePortsProperties();
222 // Apply the deserialized angle after ports are positioned so any non-rotatable element
223 // can apply its own rotatePorts() path correctly
224 element.m_orientation.applyLoadedOrientation();
225
226 qCDebug(four) << "Finished loading element.";
227}
228
229// ========== Old format (pre-4.1) ==========
230
231void GraphicElementSerializer::loadOldFormat(GraphicElement &element, QDataStream &stream, SerializationContext &context)
232{
233 loadPos(element, stream);
234 loadRotation(element, stream, context.version);
235 /* <Version1.2> */
236 loadLabel(element, stream, context.version);
237 /* <\Version1.2> */
238 /* <Version1.3> */
239 loadPortsSize(stream, context.version);
240 /* <\Version1.3> */
241 /* <Version1.9> */
242 loadTrigger(element, stream, context.version);
243 /* <\Version4.01> */
245 quint64 unusedPriority; stream >> unusedPriority;
246 }
247 /* <\Version1.9> */
248 loadInputPorts(element, stream, context);
249 loadOutputPorts(element, stream, context);
250 /* <\Version2.7> */
251 loadPixmapAppearanceNames(element, stream, context);
252}
253
254// ========== New format (4.1+) ==========
255
256void GraphicElementSerializer::loadNewFormat(GraphicElement &element, QDataStream &stream, SerializationContext &context)
257{
258 QMap<QString, QVariant> map = Serialization::readBoundedMetadata(stream);
259
260 // Check stream integrity after reading element properties map
261 if (stream.status() != QDataStream::Ok) {
262 throw PANDACEPTION("Stream error reading element properties at position %1",
263 QString::number(stream.device()->pos()));
264 }
265
266 if (map.contains("pos")) {
267 const QPointF pos = map.value("pos").toPointF();
268 validateFinitePos(pos);
269 element.setPos(pos);
270 }
271
272 if (map.contains("rotation")) {
273 const qreal angle = map.value("rotation").toReal();
274 validateFiniteAngle(angle);
275 element.m_orientation.setAngleRaw(angle);
276 }
277
278 if (map.contains("label")) {
279 element.setLabel(map.value("label").toString());
280 }
281
282 // min/max port sizes are class metadata — ignore any stale saved values.
283 // The actual port count is restored from the serialized port lists below.
284
285 // -------------------------------------------
286
287 if (map.contains("trigger")) {
288 element.setTrigger(map.value("trigger").toString());
289 }
290
291 element.m_orientation.setFlippedXRaw(map.value("flippedX", false).toBool());
292 element.m_orientation.setFlippedYRaw(map.value("flippedY", false).toBool());
293
294 // Slim PortableFile streams store one "id" per element instead of a
295 // serialId per port; the loops below re-derive each port's serialId from
296 // it, mirroring what Port::makeSerialId() produced at save time. Converted
297 // via toLongLong-then-cast so a negative id round-trips to the exact same
298 // quint64 the save side computed with static_cast<quint64>(element.id()).
299 const bool hasSavedId = map.contains("id");
300 const quint64 savedId = static_cast<quint64>(map.value("id").toLongLong());
301
302 // -------------------------------------------
303
304 QList<QMap<QString, QVariant>> inputMap = readPortList(stream, "input ports");
305
306 int port = 0;
307
308 for (const auto &input : std::as_const(inputMap)) {
309 const QString name = input.value("name").toString();
310
311 // Three shapes, newest first: explicit serialId (V4.4 through fat
312 // Rev100, and every InMemorySnapshot), derivable element id (slim
313 // Rev100), legacy neither (V4.1.9-V4.3, which carry "ptr" keys).
314 quint64 serialId;
315 if (input.contains("serialId")) {
316 serialId = input.value("serialId").toULongLong();
317 } else if (hasSavedId) {
318 // Inputs' globalIndex() is their list position.
319 serialId = Port::makeSerialId(savedId, port);
320 } else {
321 // Old format fallback: use context.nextLocalId as element basis so that
322 // serialIds remain unique even when element IDs are -1 (unassigned).
323 serialId = Port::makeSerialId(static_cast<quint64>(context.nextLocalId), port);
324 }
325
326 if (port < element.m_ports.inputs().size()) {
327 quint64 oldPtr = input.value("ptr").toULongLong();
328 // Map old pointer IDs to new serial IDs for backwards compatibility
329 if (oldPtr != 0) {
330 context.oldPtrToSerialId[oldPtr] = serialId;
331 }
332
333 if (element.elementType() == ElementType::IC) {
334 element.m_ports.inputs().value(port)->setName(name);
335 }
336 } else {
337 quint64 oldPtr = input.value("ptr").toULongLong();
338 element.m_ports.addPort(name, false);
339 // Map old pointer IDs to new serial IDs for backwards compatibility
340 if (oldPtr != 0) {
341 context.oldPtrToSerialId[oldPtr] = serialId;
342 }
343 }
344
345 context.portMap[serialId] = element.m_ports.inputs().value(port);
346
347 ++port;
348 }
349
350 removeSurplusInputs(element, static_cast<quint64>(inputMap.size()), context);
351
352 // -------------------------------------------
353
354 QList<QMap<QString, QVariant>> outputMap = readPortList(stream, "output ports");
355
356 port = 0;
357
358 for (const auto &output : std::as_const(outputMap)) {
359 const QString name = output.value("name").toString();
360
361 // Same three shapes as the input loop above.
362 quint64 serialId;
363 if (output.contains("serialId")) {
364 serialId = output.value("serialId").toULongLong();
365 } else if (hasSavedId) {
366 // Outputs' globalIndex() is inputCount + list position; use the
367 // file's own input count so derivation matches what save computed.
368 serialId = Port::makeSerialId(savedId, static_cast<int>(inputMap.size()) + port);
369 } else {
370 // Old format fallback: use context.nextLocalId as element basis so that
371 // serialIds remain unique even when element IDs are -1 (unassigned).
372 serialId = Port::makeSerialId(static_cast<quint64>(context.nextLocalId), static_cast<int>(element.m_ports.inputs().size()) + port);
373 }
374
375 if (port < element.m_ports.outputs().size()) {
376 quint64 oldPtr = output.value("ptr").toULongLong();
377 // Map old pointer IDs to new serial IDs for backwards compatibility
378 if (oldPtr != 0) {
379 context.oldPtrToSerialId[oldPtr] = serialId;
380 }
381
382 if (element.elementType() == ElementType::IC) {
383 element.m_ports.outputs().value(port)->setName(name);
384 }
385 } else {
386 quint64 oldPtr = output.value("ptr").toULongLong();
387 element.m_ports.addPort(name, true);
388 // Map old pointer IDs to new serial IDs for backwards compatibility
389 if (oldPtr != 0) {
390 context.oldPtrToSerialId[oldPtr] = serialId;
391 }
392 }
393
394 context.portMap[serialId] = element.m_ports.outputs().value(port);
395
396 ++port;
397 }
398
399 removeSurplusOutputs(element, static_cast<quint64>(outputMap.size()), context);
400
401 // -------------------------------------------
402
403 QList<QMap<QString, QVariant>> appearancesMap = readPortList(stream, "appearances");
404
405 // Unreachable: unlike Serialization::readBoundedMetadata() (which silently `break`s out of
406 // its per-entry loop on a stream error, leaving a bad status for its caller to notice),
407 // readPortList()'s own per-entry loop throws immediately on the same condition -- it can
408 // never return normally with a bad stream status for this redundant check to catch.
409 if (stream.status() != QDataStream::Ok) { // LCOV_EXCL_LINE
410 throw PANDACEPTION("Stream error reading appearances at position %1", // LCOV_EXCL_LINE
411 QString::number(stream.device()->pos())); // LCOV_EXCL_LINE
412 } // LCOV_EXCL_LINE
413
414 int index = 0;
415
416 for (const auto &entry : std::as_const(appearancesMap)) {
417 if (index >= element.m_appearance.alternativeAppearances().size()) {
418 throw PANDACEPTION("Appearance index %1 out of range (size=%2) — stream may be corrupt",
419 QString::number(index),
420 QString::number(element.m_appearance.alternativeAppearances().size()));
421 }
422
423 const QString name = entry.value("skinName").toString();
424
425 // Slim streams write default (resource) slots as empty maps to keep
426 // positional alignment; skip them — the slot keeps its compiled-in
427 // default. Must be checked BEFORE forReading(): for PortableFile it
428 // resolves "" to "" (not nullopt) and would clobber the slot.
429 if (name.isEmpty()) {
430 ++index;
431 continue;
432 }
433
434 if (const auto resolved = ExternalFilePath::forReading(name, context.contextDir, context.purpose)) {
435 element.m_appearance.setAlternativeAppearanceAt(index, *resolved);
436 }
437
438 ++index;
439 }
440
441 // If all alternative appearance slots still match the defaults, the user never
442 // applied a custom appearance — record that so the "Reset to default" action works.
444
445 element.refresh();
446
447 // Advance the per-element counter so that the next element's fallback serialIds
448 // use a different base (only relevant for V4.1.9–V4.3 files lacking serialId keys).
449 ++context.nextLocalId;
450}
451
452// ========== Property loading helpers (old format) ==========
453
454// validateFinitePos() is a static member (not a file-local helper) so its load-error
455// PANDACEPTION throw extracts under the GraphicElementSerializer tr context.
456
461void GraphicElementSerializer::validateFinitePos(const QPointF &pos)
462{
463 if (!std::isfinite(pos.x()) || !std::isfinite(pos.y())) {
464 throw PANDACEPTION("Non-finite element position — stream may be corrupt");
465 }
466}
467
468void GraphicElementSerializer::loadPos(GraphicElement &element, QDataStream &stream)
469{
470 QPointF pos; stream >> pos;
471 validateFinitePos(pos);
472 element.setPos(pos);
473}
474
479void GraphicElementSerializer::validateFiniteAngle(const qreal angle)
480{
481 if (!std::isfinite(angle)) {
482 throw PANDACEPTION("Non-finite element rotation — stream may be corrupt");
483 }
484}
485
486void GraphicElementSerializer::loadRotation(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
487{
488 qreal angle; stream >> angle;
489 validateFiniteAngle(angle);
490 element.m_orientation.setAngleRaw(angle);
491
492 // In versions before 4.1 the coordinate system for inputs and outputs was
493 // rotated 90° relative to the current convention. Apply a compensating offset
494 // so old files render correctly without migrating every saved angle.
495 if (!VersionInfo::hasQMapFormat(version)) {
496 if ((element.elementGroup() == ElementGroup::Input) || (element.elementGroup() == ElementGroup::StaticInput)) {
497 element.m_orientation.setAngleRaw(element.m_orientation.angle() + 90);
498 }
499
500 if ((element.elementGroup() == ElementGroup::Output) || (element.elementGroup() == ElementGroup::IC) || (element.elementGroup() == ElementGroup::Gate)) {
501 // Displays and Node never had the old convention applied, so skip them.
502 if ((element.elementType() == ElementType::Display7) || (element.elementType() == ElementType::Display14) || (element.elementType() == ElementType::Display16) || (element.elementType() == ElementType::Node)) {
503 return;
504 }
505
506 element.m_orientation.setAngleRaw(element.m_orientation.angle() - 90);
507 }
508 }
509}
510
511void GraphicElementSerializer::loadLabel(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
512{
513 if (VersionInfo::hasLabels(version)) {
515 }
516}
517
518void GraphicElementSerializer::loadPortsSize(QDataStream &stream, const QVersionNumber &version)
519{
520 if (VersionInfo::hasPortSizes(version)) {
521 // Drain the 4 saved values to advance the stream position.
522 // min/max port sizes are class metadata — the compiled-in values are authoritative.
523 quint64 unused; stream >> unused; stream >> unused; stream >> unused; stream >> unused;
524 }
525}
526
527void GraphicElementSerializer::loadTrigger(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
528{
529 if (VersionInfo::hasTrigger(version)) {
530 const QKeySequence trigger = Serialization::readBoundedKeySequence(stream);
531 element.setTrigger(trigger);
532 }
533}
534
535// ========== Port loading (old format) ==========
536
537void GraphicElementSerializer::loadInputPorts(GraphicElement &element, QDataStream &stream, SerializationContext &context)
538{
539 qCDebug(four) << "Loading input ports.";
540 quint64 inputSize; stream >> inputSize;
541 if (inputSize > kMaxPortsPerElement) {
542 throw PANDACEPTION("Refusing old-format input port list with implausible count %1 (max %2)",
543 QString::number(inputSize),
544 QString::number(kMaxPortsPerElement));
545 }
546
547 for (size_t port = 0; port < inputSize; ++port) {
548 loadInputPort(element, stream, context, static_cast<int>(port));
549 }
550
551 removeSurplusInputs(element, inputSize, context);
552}
553
554void GraphicElementSerializer::loadInputPort(GraphicElement &element, QDataStream &stream, SerializationContext &context, const int port)
555{
556 quint64 ptr; stream >> ptr;
557 const QString name = Serialization::readBoundedString(stream);
558 int flags; stream >> flags;
559
560 if (port < element.m_ports.inputs().size()) {
561 if (element.elementType() == ElementType::IC) {
562 element.m_ports.inputs().value(port)->setName(name);
563 }
564 } else {
565 element.m_ports.addPort(name, false);
566 }
567
568 context.portMap[ptr] = element.m_ports.inputs().value(port);
569}
570
571void GraphicElementSerializer::loadOutputPorts(GraphicElement &element, QDataStream &stream, SerializationContext &context)
572{
573 qCDebug(four) << "Loading output ports.";
574 quint64 outputSize; stream >> outputSize;
575 if (outputSize > kMaxPortsPerElement) {
576 throw PANDACEPTION("Refusing old-format output port list with implausible count %1 (max %2)",
577 QString::number(outputSize),
578 QString::number(kMaxPortsPerElement));
579 }
580
581 for (size_t port = 0; port < outputSize; ++port) {
582 loadOutputPort(element, stream, context, static_cast<int>(port));
583 }
584
585 removeSurplusOutputs(element, outputSize, context);
586}
587
588void GraphicElementSerializer::loadOutputPort(GraphicElement &element, QDataStream &stream, SerializationContext &context, const int port)
589{
590 quint64 ptr; stream >> ptr;
591 const QString name = Serialization::readBoundedString(stream);
592 int flags; stream >> flags;
593
594 if (port < element.m_ports.outputs().size()) {
595 if (element.elementType() == ElementType::IC) {
596 element.m_ports.outputs().value(port)->setName(name);
597 }
598 } else {
599 element.m_ports.addPort(name, true);
600 }
601
602 context.portMap[ptr] = element.m_ports.outputs().value(port);
603}
604
605// ========== Surplus port removal ==========
606
607void GraphicElementSerializer::removeSurplusInputs(GraphicElement &element, const quint64 inputSize_, SerializationContext &context)
608{
609 // The element may have been constructed with more ports than are stored in the file
610 // (e.g. default constructor creates minInputSize ports). Trim the excess from the end,
611 // but never go below minInputSize to avoid leaving an unusable element.
612 while ((element.inputSize() > static_cast<int>(inputSize_)) && (inputSize_ >= static_cast<quint64>(element.minInputSize()))) {
613 // Remove from the vector before deleting: a reentrant access during deserialization
614 // must never observe a dangling pointer still present in m_ports.
615 auto *deletedPort = element.m_ports.takeLastInput();
616 removePortFromMap(deletedPort, context.portMap);
617 delete deletedPort;
618 }
619}
620
621void GraphicElementSerializer::removeSurplusOutputs(GraphicElement &element, const quint64 outputSize_, SerializationContext &context)
622{
623 // Same trimming logic as removeSurplusInputs, applied to output ports
624 while ((element.outputSize() > static_cast<int>(outputSize_)) && (outputSize_ >= static_cast<quint64>(element.minOutputSize()))) {
625 // Remove from the vector before deleting: a reentrant access during deserialization
626 // must never observe a dangling pointer still present in m_ports.
627 auto *deletedPort = element.m_ports.takeLastOutput();
628 removePortFromMap(deletedPort, context.portMap);
629 delete deletedPort;
630 }
631}
632
633// ========== Appearance loading ==========
634
635void GraphicElementSerializer::loadPixmapAppearanceNames(GraphicElement &element, QDataStream &stream, SerializationContext &context)
636{
638 qCDebug(four) << "Loading pixmap appearance names.";
639 quint64 outputSize; stream >> outputSize;
640 if (outputSize > kMaxPortsPerElement) {
641 throw PANDACEPTION("Refusing old-format appearance list with implausible count %1 (max %2)",
642 QString::number(outputSize),
643 QString::number(kMaxPortsPerElement));
644 }
645
646 if (element.elementType() == ElementType::IC) {
647 // IC paints itself procedurally and has no appearance slots.
648 // Drain the saved appearance names from old file formats to keep the stream position valid.
649 for (quint64 i = 0; i < outputSize; ++i) {
650 Serialization::readBoundedString(stream); // drain appearance name
651 }
652 return;
653 }
654
655 for (size_t i = 0; i < outputSize; ++i) {
656 loadPixmapAppearanceName(element, stream, static_cast<int>(i), context);
657 }
658
660
661 element.refresh();
662 }
663}
664
665void GraphicElementSerializer::loadPixmapAppearanceName(GraphicElement &element, QDataStream &stream, const int index, const SerializationContext &context)
666{
667 const QString name = Serialization::readBoundedString(stream);
668
669 if (index >= element.m_appearance.alternativeAppearances().size()) {
670 throw PANDACEPTION("Appearance index %1 out of range (size=%2) for appearance name \"%3\" — stream may be corrupt",
671 QString::number(index), QString::number(element.m_appearance.alternativeAppearances().size()), name);
672 }
673
674 if (const auto resolved = ExternalFilePath::forReading(name, context.contextDir, context.purpose)) {
675 element.m_appearance.setAlternativeAppearanceAt(index, *resolved);
676 }
677}
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
#define qCDebug(category)
Definition Common.h:29
Single, shared implementation of how an element's external-file reference (an appearance image,...
GraphicElementSerializer: reads/writes a GraphicElement to a versioned QDataStream.
Abstract base class for all graphical circuit elements.
Port classes: Port (base), InputPort, and OutputPort.
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
Named version predicates for file-format compatibility checks.
void recomputeUsingDefault()
Recomputes the using-default flag by comparing the alternative and default lists.
const QStringList & alternativeAppearances() const
Returns the active (possibly user-customised) appearance paths.
void setAlternativeAppearanceAt(const int index, const QString &path)
Sets one alternative-appearance slot without reloading the pixmap (serializer / subclass use).
void setFlippedXRaw(bool flipped)
Sets the horizontal flip flag without re-applying the transform. Load-time only.
void setFlippedYRaw(bool flipped)
Sets the vertical flip flag without re-applying the transform. Load-time only.
qreal angle() const
Returns the current rotation angle in degrees.
void setAngleRaw(qreal angle)
Sets the angle without re-applying the transform. Load-time only.
InputPort * takeLastInput()
const QVector< OutputPort * > & outputs() const
Returns the output port vector.
void addPort(const QString &name, bool isOutput)
Appends a new port (parented to the owner) with name; isOutput selects direction.
const QVector< InputPort * > & inputs() const
Returns the input port vector.
OutputPort * takeLastOutput()
Removes and returns the last output port WITHOUT deleting it. Returns nullptr if empty.
static void save(const GraphicElement &element, QDataStream &stream, SerializationOptions options)
Writes element to stream (current keyed-QMap format).
static void load(GraphicElement &element, QDataStream &stream, SerializationContext &context)
Reads element from stream, dispatching on the format version in context.
Abstract base class for all graphical circuit elements in wiRedPanda.
ElementType elementType() const
Returns the type identifier for this element.
ElementAppearance m_appearance
int inputSize() const
Returns the current number of input ports.
qreal rotation() const
Returns the current rotation angle of this element in degrees.
bool isFlippedY() const
Returns true if this element is mirrored along the Y axis (vertical flip).
QString label() const
Returns the user-visible label text for this element.
virtual void load(QDataStream &stream, SerializationContext &context)
Loads the graphic element through a binary data stream.
void setTrigger(const QKeySequence &trigger)
Sets the keyboard shortcut to trigger and updates the label.
int minInputSize() const
Returns the minimum allowed number of input ports.
QKeySequence trigger() const
Returns the keyboard shortcut that activates this element.
void setLabel(const QString &label)
Sets the label text to label and refreshes the display.
int outputSize() const
Returns the current number of output ports.
ElementGroup elementGroup() const
Returns the group this element belongs to.
virtual void save(QDataStream &stream, SerializationOptions options) const
int minOutputSize() const
Returns the minimum allowed number of output ports.
bool isFlippedX() const
Returns true if this element is mirrored along the X axis (horizontal flip).
ElementPorts m_ports
virtual void updatePortsProperties()
Repositions and reconfigures all ports after the port count changes.
virtual void refresh()
Forces a visual refresh of the element's pixmap and ports.
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
static quint64 makeSerialId(quint64 elementBase, int globalIndex)
Definition Port.cpp:156
static QString readBoundedString(QDataStream &stream)
Reads a QString from stream, refusing to allocate more bytes than remain in the stream (prevents OOM ...
static QKeySequence readBoundedKeySequence(QDataStream &stream)
Reads a QKeySequence from stream, rejecting an implausible/oversized internal key-combination count b...
static QMap< QString, QVariant > readBoundedMetadata(QDataStream &stream)
Reads the file-level metadata QMap<QString,QVariant> from stream without calling QList::reserve() wit...
std::optional< QString > forReading(const QString &storedValue, const QString &contextDir, SerializationPurpose purpose)
QString forWriting(const QString &path, SerializationPurpose purpose)
bool hasQMapFormat(const QVersionNumber &v)
V4.1: Format changed from flat binary to keyed QMap; port serial IDs; rotation fix.
Definition VersionInfo.h:60
bool hasTrigger(const QVersionNumber &v)
V1.9: Keyboard trigger sequences added.
Definition VersionInfo.h:39
bool hasUnusedPriority(const QVersionNumber &v)
V4.0.1: Unused priority field in old format.
Definition VersionInfo.h:57
bool hasAppearanceNames(const QVersionNumber &v)
V2.7: Pixmap appearance names stored in file.
Definition VersionInfo.h:45
bool hasPortSizes(const QVersionNumber &v)
V1.3: Port-size constraints stored in file.
Definition VersionInfo.h:27
bool hasLabels(const QVersionNumber &v)
V1.2: Labels added to elements; IC filename field.
Definition VersionInfo.h:24
Bundles all per-deserialization state so that load() overrides receive it through one explicit parame...
QVersionNumber version
File-format version read from the stream header.
QHash< quint64, quint64 > oldPtrToSerialId
SerializationPurpose purpose
What this deserialization is for; see SerializationPurpose.
QHash< quint64, Port * > & portMap
Accumulated port-pointer map built during deserialization.
QString contextDir
Directory of the .panda file (for relative path resolution).
Options passed to GraphicElement::save() (and friends); the save-side counterpart of SerializationCon...
SerializationPurpose purpose
What this serialization is for; see SerializationPurpose. Mandatory, no default.