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 if (stream.status() != QDataStream::Ok) {
406 throw PANDACEPTION("Stream error reading appearances at position %1",
407 QString::number(stream.device()->pos()));
408 }
409
410 int index = 0;
411
412 for (const auto &entry : std::as_const(appearancesMap)) {
413 if (index >= element.m_appearance.alternativeAppearances().size()) {
414 throw PANDACEPTION("Appearance index %1 out of range (size=%2) — stream may be corrupt",
415 QString::number(index),
416 QString::number(element.m_appearance.alternativeAppearances().size()));
417 }
418
419 const QString name = entry.value("skinName").toString();
420
421 // Slim streams write default (resource) slots as empty maps to keep
422 // positional alignment; skip them — the slot keeps its compiled-in
423 // default. Must be checked BEFORE forReading(): for PortableFile it
424 // resolves "" to "" (not nullopt) and would clobber the slot.
425 if (name.isEmpty()) {
426 ++index;
427 continue;
428 }
429
430 if (const auto resolved = ExternalFilePath::forReading(name, context.contextDir, context.purpose)) {
431 element.m_appearance.setAlternativeAppearanceAt(index, *resolved);
432 }
433
434 ++index;
435 }
436
437 // If all alternative appearance slots still match the defaults, the user never
438 // applied a custom appearance — record that so the "Reset to default" action works.
440
441 element.refresh();
442
443 // Advance the per-element counter so that the next element's fallback serialIds
444 // use a different base (only relevant for V4.1.9–V4.3 files lacking serialId keys).
445 ++context.nextLocalId;
446}
447
448// ========== Property loading helpers (old format) ==========
449
450// validateFinitePos() is a static member (not a file-local helper) so its load-error
451// PANDACEPTION throw extracts under the GraphicElementSerializer tr context.
452
457void GraphicElementSerializer::validateFinitePos(const QPointF &pos)
458{
459 if (!std::isfinite(pos.x()) || !std::isfinite(pos.y())) {
460 throw PANDACEPTION("Non-finite element position — stream may be corrupt");
461 }
462}
463
464void GraphicElementSerializer::loadPos(GraphicElement &element, QDataStream &stream)
465{
466 QPointF pos; stream >> pos;
467 validateFinitePos(pos);
468 element.setPos(pos);
469}
470
475void GraphicElementSerializer::validateFiniteAngle(const qreal angle)
476{
477 if (!std::isfinite(angle)) {
478 throw PANDACEPTION("Non-finite element rotation — stream may be corrupt");
479 }
480}
481
482void GraphicElementSerializer::loadRotation(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
483{
484 qreal angle; stream >> angle;
485 validateFiniteAngle(angle);
486 element.m_orientation.setAngleRaw(angle);
487
488 // In versions before 4.1 the coordinate system for inputs and outputs was
489 // rotated 90° relative to the current convention. Apply a compensating offset
490 // so old files render correctly without migrating every saved angle.
491 if (!VersionInfo::hasQMapFormat(version)) {
492 if ((element.elementGroup() == ElementGroup::Input) || (element.elementGroup() == ElementGroup::StaticInput)) {
493 element.m_orientation.setAngleRaw(element.m_orientation.angle() + 90);
494 }
495
496 if ((element.elementGroup() == ElementGroup::Output) || (element.elementGroup() == ElementGroup::IC) || (element.elementGroup() == ElementGroup::Gate)) {
497 // Displays and Node never had the old convention applied, so skip them.
498 if ((element.elementType() == ElementType::Display7) || (element.elementType() == ElementType::Display14) || (element.elementType() == ElementType::Display16) || (element.elementType() == ElementType::Node)) {
499 return;
500 }
501
502 element.m_orientation.setAngleRaw(element.m_orientation.angle() - 90);
503 }
504 }
505}
506
507void GraphicElementSerializer::loadLabel(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
508{
509 if (VersionInfo::hasLabels(version)) {
511 }
512}
513
514void GraphicElementSerializer::loadPortsSize(QDataStream &stream, const QVersionNumber &version)
515{
516 if (VersionInfo::hasPortSizes(version)) {
517 // Drain the 4 saved values to advance the stream position.
518 // min/max port sizes are class metadata — the compiled-in values are authoritative.
519 quint64 unused; stream >> unused; stream >> unused; stream >> unused; stream >> unused;
520 }
521}
522
523void GraphicElementSerializer::loadTrigger(GraphicElement &element, QDataStream &stream, const QVersionNumber &version)
524{
525 if (VersionInfo::hasTrigger(version)) {
526 const QKeySequence trigger = Serialization::readBoundedKeySequence(stream);
527 element.setTrigger(trigger);
528 }
529}
530
531// ========== Port loading (old format) ==========
532
533void GraphicElementSerializer::loadInputPorts(GraphicElement &element, QDataStream &stream, SerializationContext &context)
534{
535 qCDebug(four) << "Loading input ports.";
536 quint64 inputSize; stream >> inputSize;
537 if (inputSize > kMaxPortsPerElement) {
538 throw PANDACEPTION("Refusing old-format input port list with implausible count %1 (max %2)",
539 QString::number(inputSize),
540 QString::number(kMaxPortsPerElement));
541 }
542
543 for (size_t port = 0; port < inputSize; ++port) {
544 loadInputPort(element, stream, context, static_cast<int>(port));
545 }
546
547 removeSurplusInputs(element, inputSize, context);
548}
549
550void GraphicElementSerializer::loadInputPort(GraphicElement &element, QDataStream &stream, SerializationContext &context, const int port)
551{
552 quint64 ptr; stream >> ptr;
553 const QString name = Serialization::readBoundedString(stream);
554 int flags; stream >> flags;
555
556 if (port < element.m_ports.inputs().size()) {
557 if (element.elementType() == ElementType::IC) {
558 element.m_ports.inputs().value(port)->setName(name);
559 }
560 } else {
561 element.m_ports.addPort(name, false);
562 }
563
564 context.portMap[ptr] = element.m_ports.inputs().value(port);
565}
566
567void GraphicElementSerializer::loadOutputPorts(GraphicElement &element, QDataStream &stream, SerializationContext &context)
568{
569 qCDebug(four) << "Loading output ports.";
570 quint64 outputSize; stream >> outputSize;
571 if (outputSize > kMaxPortsPerElement) {
572 throw PANDACEPTION("Refusing old-format output port list with implausible count %1 (max %2)",
573 QString::number(outputSize),
574 QString::number(kMaxPortsPerElement));
575 }
576
577 for (size_t port = 0; port < outputSize; ++port) {
578 loadOutputPort(element, stream, context, static_cast<int>(port));
579 }
580
581 removeSurplusOutputs(element, outputSize, context);
582}
583
584void GraphicElementSerializer::loadOutputPort(GraphicElement &element, QDataStream &stream, SerializationContext &context, const int port)
585{
586 quint64 ptr; stream >> ptr;
587 const QString name = Serialization::readBoundedString(stream);
588 int flags; stream >> flags;
589
590 if (port < element.m_ports.outputs().size()) {
591 if (element.elementType() == ElementType::IC) {
592 element.m_ports.outputs().value(port)->setName(name);
593 }
594 } else {
595 element.m_ports.addPort(name, true);
596 }
597
598 context.portMap[ptr] = element.m_ports.outputs().value(port);
599}
600
601// ========== Surplus port removal ==========
602
603void GraphicElementSerializer::removeSurplusInputs(GraphicElement &element, const quint64 inputSize_, SerializationContext &context)
604{
605 // The element may have been constructed with more ports than are stored in the file
606 // (e.g. default constructor creates minInputSize ports). Trim the excess from the end,
607 // but never go below minInputSize to avoid leaving an unusable element.
608 while ((element.inputSize() > static_cast<int>(inputSize_)) && (inputSize_ >= static_cast<quint64>(element.minInputSize()))) {
609 // Remove from the vector before deleting: a reentrant access during deserialization
610 // must never observe a dangling pointer still present in m_ports.
611 auto *deletedPort = element.m_ports.takeLastInput();
612 removePortFromMap(deletedPort, context.portMap);
613 delete deletedPort;
614 }
615}
616
617void GraphicElementSerializer::removeSurplusOutputs(GraphicElement &element, const quint64 outputSize_, SerializationContext &context)
618{
619 // Same trimming logic as removeSurplusInputs, applied to output ports
620 while ((element.outputSize() > static_cast<int>(outputSize_)) && (outputSize_ >= static_cast<quint64>(element.minOutputSize()))) {
621 // Remove from the vector before deleting: a reentrant access during deserialization
622 // must never observe a dangling pointer still present in m_ports.
623 auto *deletedPort = element.m_ports.takeLastOutput();
624 removePortFromMap(deletedPort, context.portMap);
625 delete deletedPort;
626 }
627}
628
629// ========== Appearance loading ==========
630
631void GraphicElementSerializer::loadPixmapAppearanceNames(GraphicElement &element, QDataStream &stream, SerializationContext &context)
632{
634 qCDebug(four) << "Loading pixmap appearance names.";
635 quint64 outputSize; stream >> outputSize;
636 if (outputSize > kMaxPortsPerElement) {
637 throw PANDACEPTION("Refusing old-format appearance list with implausible count %1 (max %2)",
638 QString::number(outputSize),
639 QString::number(kMaxPortsPerElement));
640 }
641
642 if (element.elementType() == ElementType::IC) {
643 // IC paints itself procedurally and has no appearance slots.
644 // Drain the saved appearance names from old file formats to keep the stream position valid.
645 for (quint64 i = 0; i < outputSize; ++i) {
646 Serialization::readBoundedString(stream); // drain appearance name
647 }
648 return;
649 }
650
651 for (size_t i = 0; i < outputSize; ++i) {
652 loadPixmapAppearanceName(element, stream, static_cast<int>(i), context);
653 }
654
656
657 element.refresh();
658 }
659}
660
661void GraphicElementSerializer::loadPixmapAppearanceName(GraphicElement &element, QDataStream &stream, const int index, const SerializationContext &context)
662{
663 const QString name = Serialization::readBoundedString(stream);
664
665 if (index >= element.m_appearance.alternativeAppearances().size()) {
666 throw PANDACEPTION("Appearance index %1 out of range (size=%2) for appearance name \"%3\" — stream may be corrupt",
667 QString::number(index), QString::number(element.m_appearance.alternativeAppearances().size()), name);
668 }
669
670 if (const auto resolved = ExternalFilePath::forReading(name, context.contextDir, context.purpose)) {
671 element.m_appearance.setAlternativeAppearanceAt(index, *resolved);
672 }
673}
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.