wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ClipboardManager.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
5
6#include <memory>
7
8#include <QApplication>
9#include <QClipboard>
10#include <QDrag>
11#include <QIODevice>
12#include <QLoggingCategory>
13#include <QMimeData>
14#include <QPainter>
15
16#include "App/Core/Common.h"
17#include "App/Core/Constants.h"
18#include "App/Core/MimeTypes.h"
23#include "App/Scene/Commands.h"
25#include "App/Scene/Scene.h"
28
30 : m_scene(scene)
31{
32}
33
35{
36 if (m_scene->selectedElements().empty()) {
37 QApplication::clipboard()->clear();
38 return;
39 }
40
41 QByteArray itemData;
42 QDataStream stream(&itemData, QIODevice::WriteOnly);
44 serializeItems(m_scene->selectedItems(), stream);
45
46 auto mimeData = std::make_unique<QMimeData>();
47 mimeData->setData(MimeType::Clipboard, itemData);
48
49 // Include only blobs used by the selected elements
50 auto *registry = m_scene->icRegistry();
51 QMap<QString, QByteArray> usedBlobs;
52 for (auto *elm : m_scene->selectedElements()) {
53 if (elm->isEmbedded() && !elm->blobName().isEmpty()) {
54 const QString &name = elm->blobName();
55 if (!usedBlobs.contains(name) && registry->hasBlob(name)) {
56 usedBlobs[name] = registry->blob(name);
57 }
58 }
59 }
60 if (!usedBlobs.isEmpty()) {
61 // Unversioned copy for backward compatibility with older app versions.
62 QByteArray regBytes;
63 QDataStream regStream(&regBytes, QIODevice::WriteOnly);
64 regStream << usedBlobs;
65 mimeData->setData(MimeType::BlobRegistry, regBytes);
66
67 // Versioned copy (Qt_5_12) for newer app versions — preferred on paste.
68 QByteArray regBytesV2;
69 QDataStream regStreamV2(&regBytesV2, QIODevice::WriteOnly);
70 regStreamV2.setVersion(QDataStream::Qt_5_12);
71 regStreamV2 << usedBlobs;
72 mimeData->setData(MimeType::BlobRegistryV2, regBytesV2);
73 }
74
75 QApplication::clipboard()->setMimeData(mimeData.release());
76}
77
79{
80 if (m_scene->selectedElements().isEmpty()) {
81 QApplication::clipboard()->clear();
82 return;
83 }
84
85 // Collect used blobs before deleting elements
86 auto *registry = m_scene->icRegistry();
87 QMap<QString, QByteArray> usedBlobs;
88 for (auto *elm : m_scene->selectedElements()) {
89 if (elm->isEmbedded() && !elm->blobName().isEmpty()) {
90 const QString &name = elm->blobName();
91 if (!usedBlobs.contains(name) && registry->hasBlob(name)) {
92 usedBlobs[name] = registry->blob(name);
93 }
94 }
95 }
96
97 QByteArray itemData;
98 QDataStream stream(&itemData, QIODevice::WriteOnly);
100 serializeAndDelete(m_scene->selectedItems(), stream);
101
102 auto mimeData = std::make_unique<QMimeData>();
103 mimeData->setData(MimeType::Clipboard, itemData);
104
105 if (!usedBlobs.isEmpty()) {
106 // Unversioned copy for backward compatibility with older app versions.
107 QByteArray regBytes;
108 QDataStream regStream(&regBytes, QIODevice::WriteOnly);
109 regStream << usedBlobs;
110 mimeData->setData(MimeType::BlobRegistry, regBytes);
111
112 // Versioned copy (Qt_5_12) for newer app versions — preferred on paste.
113 QByteArray regBytesV2;
114 QDataStream regStreamV2(&regBytesV2, QIODevice::WriteOnly);
115 regStreamV2.setVersion(QDataStream::Qt_5_12);
116 regStreamV2 << usedBlobs;
117 mimeData->setData(MimeType::BlobRegistryV2, regBytesV2);
118 }
119
120 QApplication::clipboard()->setMimeData(mimeData.release());
121}
122
124{
125 const auto *mimeData = QApplication::clipboard()->mimeData();
126
127 if (!mimeData) {
128 return;
129 }
130
131 // Import blob registry from clipboard so cross-tab paste of embedded ICs works.
132 // Prefer BlobRegistryV2 (explicit Qt_5_12 version); fall back to the legacy
133 // unversioned BlobRegistry produced by older app versions.
134 QMap<QString, QByteArray> clipboardBlobs;
135 if (mimeData->hasFormat(MimeType::BlobRegistryV2)) {
136 QByteArray regBytes = mimeData->data(MimeType::BlobRegistryV2);
137 QDataStream regStream(&regBytes, QIODevice::ReadOnly);
138 regStream.setVersion(QDataStream::Qt_5_12);
139 try { clipboardBlobs = Serialization::readBoundedBlobMap(regStream); } catch (...) {}
140 } else if (mimeData->hasFormat(MimeType::BlobRegistry)) {
141 QByteArray regBytes = mimeData->data(MimeType::BlobRegistry);
142 QDataStream regStream(&regBytes, QIODevice::ReadOnly);
143 try { clipboardBlobs = Serialization::readBoundedBlobMap(regStream); } catch (...) {}
144 }
145 QByteArray itemData;
146
147 if (mimeData->hasFormat(MimeType::ClipboardLegacy)) {
148 itemData = mimeData->data(MimeType::ClipboardLegacy);
149 }
150
151 if (mimeData->hasFormat(MimeType::Clipboard)) {
152 itemData = mimeData->data(MimeType::Clipboard);
153 }
154
155 // Register any new embedded-IC blobs and add the pasted items as a single undo step —
156 // registering the blobs untracked would leave them orphaned in the registry forever after
157 // an undo, since AddItemsCommand only knows about scene items, not the blob registry.
158 const bool needsMacro = !clipboardBlobs.isEmpty() && !itemData.isEmpty();
159 if (needsMacro) {
160 m_scene->undoStack()->beginMacro(tr("Paste"));
161 }
162
163 if (!clipboardBlobs.isEmpty()) {
164 auto *registry = m_scene->icRegistry();
165 for (auto it = clipboardBlobs.cbegin(); it != clipboardBlobs.cend(); ++it) {
166 if (!registry->hasBlob(it.key())) {
167 m_scene->receiveCommand(new RegisterBlobCommand(it.key(), it.value(), m_scene));
168 }
169 }
170 }
171
172 if (!itemData.isEmpty()) {
173 QDataStream stream(&itemData, QIODevice::ReadOnly);
174 QVersionNumber version = Serialization::readPandaHeader(stream);
175 deserializeAndAdd(stream, version);
176 }
177
178 if (needsMacro) {
179 m_scene->undoStack()->endMacro();
180 }
181}
182
183bool ClipboardManager::canPaste(const QMimeData *mimeData)
184{
185 // Mirrors the formats paste() reads above — copy()/cut() write only the
186 // current format, while older app versions wrote the legacy one.
187 return mimeData
188 && (mimeData->hasFormat(MimeType::Clipboard) || mimeData->hasFormat(MimeType::ClipboardLegacy));
189}
190
191QImage ClipboardManager::buildDragImage(Scene *scene, const QTransform &viewTransform, const QRectF &sourceRect)
192{
193 QSize mappedSize = viewTransform.mapRect(sourceRect).size().toSize();
194
195 // Cap the ghost image at a sane maximum dimension. sourceRect derives from selected
196 // elements' scene positions (a crafted/corrupted file, or a large paste offset, can place
197 // one at an extreme-but-finite coordinate — the same gap CircuitExporter::renderToImage
198 // hit), so mappedSize would otherwise be sized proportionally to that distance. Scale down
199 // to fit instead of failing outright; the ghost is a transient visual aid, not saved output.
200 if (mappedSize.width() > kMaxDragImageDimension || mappedSize.height() > kMaxDragImageDimension) {
201 mappedSize.scale(kMaxDragImageDimension, kMaxDragImageDimension, Qt::KeepAspectRatio);
202 }
203
204 QImage image(mappedSize, QImage::Format_ARGB32_Premultiplied);
205 image.fill(Qt::transparent);
206
207 QPainter painter(&image);
208 // Opacity 0 makes the ghost transparent; the drag cursor shape still appears
209 painter.setOpacity(0.0);
210 scene->render(&painter, QRectF(QPointF(), mappedSize), sourceRect);
211 painter.end();
212
213 return image;
214}
215
216void ClipboardManager::cloneDrag(const QPointF &mousePos)
217{
218 auto *view = m_scene->view();
219 sentryBreadcrumb("ui", QStringLiteral("Clone drag started"));
220 qCDebug(zero) << "Ctrl + Drag action triggered.";
221 const auto selectedElements = m_scene->selectedElements();
222
223 if (selectedElements.isEmpty()) {
224 return;
225 }
226
227 // --- Build drag pixmap ---
228 // Temporarily hide non-selected items so the rendered image shows only
229 // the selection, giving the drag ghost the correct visual appearance
230 const auto items = m_scene->items();
231
232 for (auto *item : items) {
233 if (((item->type() == GraphicElement::Type) || (item->type() == Connection::Type)) && !item->isSelected()) {
234 item->hide();
235 }
236 }
237
238 QRectF rect;
239
240 for (auto *element : selectedElements) {
241 rect = rect.united(element->sceneBoundingRect());
242 }
243
244 // 8px padding avoids clipping port handles at the bounding-rect edges
245 rect = rect.adjusted(-8, -8, 8, 8);
246
247 const QImage image = buildDragImage(m_scene, view->transform(), rect);
248
249 // Restore hidden items before the drag begins so the scene looks normal
250 for (auto *item : items) {
251 if (((item->type() == GraphicElement::Type) || (item->type() == Connection::Type)) && !item->isSelected()) {
252 item->show();
253 }
254 }
255
256 // --- Serialize selection for drop target ---
257 QByteArray itemData;
258 QDataStream stream(&itemData, QIODevice::WriteOnly);
260 // Embed the mouse-press position so the drop handler can compute the correct offset
261 stream << mousePos;
262 serializeItems(m_scene->selectedItems(), stream);
263
264 auto mimeData = std::make_unique<QMimeData>();
265 mimeData->setData(MimeType::CloneDrag, itemData);
266
267 auto *drag = new QDrag(m_scene);
268 drag->setMimeData(mimeData.release());
269 drag->setPixmap(QPixmap::fromImage(image));
270 // Hot-spot aligns the drag image to the original element positions under the cursor
271 QPointF offset = view->transform().map(mousePos - rect.topLeft());
272 drag->setHotSpot(offset.toPoint());
273 SimulationBlocker blocker(m_scene->simulation());
274 drag->exec(Qt::CopyAction, Qt::CopyAction);
275}
276
277// --- Private helpers ---
278
279void ClipboardManager::serializeItems(const QList<QGraphicsItem *> &items, QDataStream &stream)
280{
281 // Compute the centroid of all selected elements (not connections) so that
282 // paste can place the clipboard contents relative to the cursor position
283 QPointF center(0.0, 0.0);
284 int itemsQuantity = 0;
285
286 for (auto *item : items) {
287 if (item->type() == GraphicElement::Type) {
288 center += item->pos();
289 ++itemsQuantity;
290 }
291 }
292
293 stream << center / static_cast<qreal>(itemsQuantity);
295}
296
297void ClipboardManager::serializeAndDelete(const QList<QGraphicsItem *> &items, QDataStream &stream)
298{
299 serializeItems(items, stream);
300 m_scene->deleteAction();
301}
302
303QList<QGraphicsItem *> ClipboardManager::deserializeAndAdd(QDataStream &stream, const QVersionNumber &version,
304 std::optional<QPointF> fixedOffset)
305{
306 m_scene->clearSelection();
307
308 QPointF center; stream >> center;
309
310 QHash<quint64, Port *> portMap;
311 auto context = m_scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
312 const auto itemList = Serialization::deserialize(stream, context);
313 // Paste: shift elements so their centroid lands at the cursor, then nudge 32 px
314 // diagonally so repeated pastes don't completely overlap. Duplicate: shift by exactly
315 // fixedOffset from the originals so the copies sit a grid step down-right, in place.
316 const QPointF offset = fixedOffset ? *fixedOffset
317 : (m_scene->mousePos() - center - QPointF(32.0, 32.0));
318
319 m_scene->receiveCommand(new AddItemsCommand(itemList, m_scene));
320
321 for (auto *item : itemList) {
322 if (item->type() == GraphicElement::Type) {
323 item->setPos((item->pos() + offset));
324 }
325 }
326
327 m_scene->resizeScene();
328 return itemList;
329}
330
332{
333 if (m_scene->selectedElements().empty()) {
334 return;
335 }
336
337 // Serialize the selection to a private buffer so the system clipboard is left untouched.
338 QByteArray itemData;
339 QDataStream writeStream(&itemData, QIODevice::WriteOnly);
341 serializeItems(m_scene->selectedItems(), writeStream);
342
343 // Embedded-IC blobs referenced by the copies already live in this scene's registry
344 // (same tab), so — unlike paste — no blob re-registration is needed.
345 QDataStream readStream(&itemData, QIODevice::ReadOnly);
346 const QVersionNumber version = Serialization::readPandaHeader(readStream);
347 const QPointF step(Constants::gridSize, Constants::gridSize);
348 const auto added = deserializeAndAdd(readStream, version, step);
349
350 // Make the copies the active selection so they can be moved right away.
351 for (auto *item : added) {
352 item->setSelected(true);
353 }
354}
ClipboardManager: handles copy, cut, paste and clone-drag operations.
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define qCDebug(category)
Definition Common.h:29
Connection: a wire that connects an output port to an input port in the circuit scene.
Shared numeric constants used across layers.
Abstract base class for all graphical circuit elements.
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
MIME type string constants for drag-and-drop operations.
Main circuit editing scene with undo/redo and user interaction.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
RAII guard that temporarily stops the simulation while in scope.
void paste()
Pastes items from the system clipboard into the scene.
static QImage buildDragImage(Scene *scene, const QTransform &viewTransform, const QRectF &sourceRect)
Renders sourceRect of scene, mapped through viewTransform, into a transparent-filled QImage bounded b...
void cut()
Cuts the currently selected items (copy + delete).
ClipboardManager(Scene *scene)
void cloneDrag(const QPointF &mousePos)
Initiates a clone-drag (Ctrl+drag) of the current selection.
static bool canPaste(const QMimeData *mimeData)
Returns whether mimeData carries circuit data paste() accepts.
void copy()
Copies the currently selected items to the system clipboard.
static constexpr int kMaxDragImageDimension
Undo command that registers/unregisters a blob in the IC registry.
Definition Commands.h:450
Main circuit editing scene.
Definition Scene.h:56
GraphicsView * view() const
Returns the GraphicsView currently displaying this scene.
Definition Scene.cpp:743
static QList< QGraphicsItem * > deserialize(QDataStream &stream, SerializationContext &context)
Deserializes items from stream until the stream is exhausted.
static void serialize(const QList< QGraphicsItem * > &items, QDataStream &stream, SerializationOptions options)
Serializes items to stream in the current .panda binary format.
static QMap< QString, QByteArray > readBoundedBlobMap(QDataStream &stream)
Reads a QMap<QString,QByteArray> from stream with bounds checking.
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
static QVersionNumber readPandaHeader(QDataStream &stream)
Reads and validates the .panda circuit file header; returns the stored version number.
RAII guard that stops the simulation on construction and restarts it on destruction.
constexpr int gridSize
Scene grid unit in pixels (elements snap to gridSize/2).
Definition Constants.h:12
constexpr const char * CloneDrag
Current MIME type for clone-drag (Ctrl+drag within the scene).
Definition MimeTypes.h:18
constexpr const char * ClipboardLegacy
Legacy clipboard MIME type retained for backward compatibility.
Definition MimeTypes.h:25
constexpr const char * BlobRegistry
Definition MimeTypes.h:29
constexpr const char * BlobRegistryV2
Definition MimeTypes.h:33
constexpr const char * Clipboard
Current MIME type for clipboard copy/paste data.
Definition MimeTypes.h:23