wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ICController.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 <QByteArray>
7#include <QDir>
8#include <QFile>
9#include <QFileInfo>
10#include <QInputDialog>
11#include <QLineEdit>
12#include <QMessageBox>
13#include <QPushButton>
14
16#include "App/Core/Common.h"
17#include "App/Core/Enums.h"
19#include "App/Element/IC.h"
20#include "App/IO/FileUtils.h"
22#include "App/Scene/Commands.h"
24#include "App/Scene/Scene.h"
25#include "App/Scene/Workspace.h"
30
32 : QObject(parent)
33 , m_host(host)
34{
35}
36
37IC *ICController::selectedIC() const
38{
39 auto *tab = m_host.currentTab();
40 if (!tab) {
41 return nullptr;
42 }
43
44 const auto selected = tab->scene()->selectedElements();
45 if (selected.isEmpty()) {
46 return nullptr;
47 }
48
49 if (selected.first()->elementType() != ElementType::IC) {
50 return nullptr;
51 }
52
53 return static_cast<IC *>(selected.first());
54}
55
57{
58 Application::guardedSlot(this, [this] {
59 sentryBreadcrumb("ic", QStringLiteral("Add IC"));
60 auto *tab = m_host.currentTab();
61 if (!tab) {
62 return;
63 }
64
65 // The IC list is directory-relative. If the project hasn't been saved yet there's no
66 // directory to copy into, so offer to save now instead of dead-ending on an error.
67 if (!ensureProjectSaved(tab->scene())) {
68 return;
69 }
70
71 const QString selectedFile = FileDialogs::provider()->getOpenFileName(m_host.widget(), tr("Open File"), QString(), tr("Panda") + " (*.panda)");
72
73 if (selectedFile.isEmpty()) {
74 return;
75 }
76
77 const QStringList files = {selectedFile};
78
79 QMessageBox::information(m_host.widget(), tr("Info"), tr("Selected files (and their dependencies) will be copied to the current project folder."));
80
81 // Copy the chosen .panda file (and any ICs it depends on transitively)
82 // into the project's directory so that relative paths work when reopened.
83 for (const auto &file : files) {
84 const QFileInfo srcInfo(file);
85 QFileInfo destPath(m_host.currentDir().absolutePath() + "/" + srcInfo.fileName());
86
87 // A *different* file with the same name already in the project folder would make
88 // copyPandaFile() skip the copy, silently binding the IC to that pre-existing
89 // file's content. Warn and let the user replace it or keep the existing one.
90 if (destPath.exists() && !FileUtils::filesHaveSameContent(srcInfo, destPath)) {
91 QMessageBox box(QMessageBox::Warning, tr("File name conflict"),
92 tr("A different file named \"%1\" already exists in the project folder.").arg(srcInfo.fileName()),
93 QMessageBox::NoButton, m_host.widget());
94 auto *replaceButton = box.addButton(tr("Replace"), QMessageBox::AcceptRole);
95 auto *keepButton = box.addButton(tr("Keep Existing"), QMessageBox::RejectRole);
96 box.addButton(QMessageBox::Cancel);
97 box.exec();
98
99 if (box.clickedButton() == replaceButton) {
100 QFile::remove(destPath.absoluteFilePath()); // let copyPandaFile write the new file
101 } else if (box.clickedButton() == keepButton) {
102 continue; // bind to the existing file; don't overwrite or pull in the source's deps
103 } else {
104 return; // Cancel
105 }
106 }
107
108 Serialization::copyPandaFile(srcInfo, destPath);
109 }
110
111 m_host.palette()->updateICList(m_host.icListFile());
112 });
113}
114
116{
117 Application::guardedSlot(this, [this] {
118 sentryBreadcrumb("ic", QStringLiteral("Remove IC"));
119 QMessageBox::information(m_host.widget(), tr("Info"), tr("Drag here to remove."));
120 });
121}
122
123void ICController::removeICFile(const QString &icFileName)
124{
125 auto *tab = m_host.currentTab();
126 if (!tab) {
127 return;
128 }
129
130 QList<QGraphicsItem *> toDelete;
131 for (auto *element : tab->scene()->elements()) {
132 if (element->elementType() != ElementType::IC) {
133 continue;
134 }
135 const auto *ic = qobject_cast<IC *>(element);
136 if (ic && !ic->isEmbedded() && QFileInfo(ic->file()).fileName().toLower() == icFileName.toLower()) {
137 toDelete.append(element);
138 }
139 }
140
141 // Move the source file to the system trash first: a mistaken removal stays recoverable
142 // (unlike a hard delete), and bailing out here on failure means the scene instances are
143 // never deleted while the file is still present.
144 QFile file(m_host.currentDir().absolutePath() + "/" + icFileName);
145 if (file.exists() && !file.moveToTrash()) {
146 throw PANDACEPTION("Error moving file to trash: %1", file.errorString());
147 }
148
149 if (!toDelete.isEmpty()) {
150 tab->scene()->receiveCommand(new DeleteItemsCommand(toDelete, tab->scene()));
151 tab->simulation()->restart();
152 }
153
154 m_host.palette()->updateICList(m_host.icListFile());
155 // Auto-save after removal so the scene no longer references the removed file.
156 m_host.requestSave();
157}
158
159QString ICController::resolveUniqueBlobName(const QString &initialName, Scene *scene)
160{
161 auto *reg = scene->icRegistry();
162 QString blobName = reg->uniqueBlobName(initialName.trimmed());
163
164 // If the auto-generated name differs from the initial, let the user confirm or override
165 if (blobName != initialName.trimmed()) {
166 bool ok = false;
167 blobName = QInputDialog::getText(m_host.widget(),
168 tr("Name Collision"),
169 tr("An embedded IC named \"%1\" already exists.\nSuggested name:").arg(initialName.trimmed()),
170 QLineEdit::Normal, blobName, &ok);
171 blobName = blobName.trimmed();
172 if (!ok || blobName.isEmpty()) {
173 return {};
174 }
175 // Re-check in case the user typed a name that also collides
176 if (reg->hasBlob(blobName)) {
177 blobName = reg->uniqueBlobName(blobName);
178 }
179 }
180 return blobName;
181}
182
183bool ICController::ensureProjectSaved(Scene *scene)
184{
185 if (scene && !scene->contextDir().isEmpty()) {
186 return true; // already backed by a real directory
187 }
188
189 const auto choice = QMessageBox::question(m_host.widget(), tr("Save required"),
190 tr("This action needs the project saved to a file first, so IC paths can be resolved.\n\nSave it now?"),
191 QMessageBox::Save | QMessageBox::Cancel, QMessageBox::Save);
192 if (choice != QMessageBox::Save) {
193 return false;
194 }
195
196 m_host.requestSave(); // modal; may prompt for a path and returns once done or cancelled
197 return scene && !scene->contextDir().isEmpty();
198}
199
201{
202 sentryBreadcrumb("ic", QStringLiteral("Embed IC"));
203 auto *firstIC = selectedIC();
204 if (!firstIC || firstIC->file().isEmpty()) {
205 return;
206 }
207
208 auto *scene = m_host.currentTab()->scene();
209
210 if (!ensureProjectSaved(scene)) {
211 return;
212 }
213 const QString contextDir = scene->contextDir();
214
215 QString blobName = resolveUniqueBlobName(QFileInfo(firstIC->file()).baseName(), scene);
216 if (blobName.isEmpty()) {
217 return;
218 }
219
220 QFile file(QDir(contextDir).absoluteFilePath(firstIC->file()));
221 if (!file.open(QIODevice::ReadOnly)) {
222 QMessageBox::warning(m_host.widget(), tr("Error"), tr("Could not read IC file: %1").arg(file.errorString()));
223 return;
224 }
225 QByteArray fileBytes = file.readAll();
226 file.close();
227
228 scene->icRegistry()->embedICsByFile(firstIC->file(), fileBytes, blobName);
229 m_host.palette()->updateEmbeddedICList(scene);
230 m_host.showStatusMessage(tr("IC embedded successfully."), 4000);
231}
232
234{
235 sentryBreadcrumb("ic", QStringLiteral("Extract IC"));
236 auto *firstIC = selectedIC();
237 if (!firstIC || !firstIC->isEmbedded()) {
238 return;
239 }
240
241 auto *scene = m_host.currentTab()->scene();
242 const QString blobName = firstIC->blobName();
243 if (!ensureProjectSaved(scene)) {
244 return;
245 }
246 const QString contextDir = scene->contextDir();
247
248 const QString suggestion = QDir(contextDir).absoluteFilePath(blobName + ".panda");
249 QString fileName = FileDialogs::provider()->getSaveFileName(m_host.widget(), tr("Extract IC to file..."), suggestion, tr("Panda files") + " (*.panda)").fileName;
250
251 if (fileName.isEmpty()) {
252 return;
253 }
254
255 if (!fileName.endsWith(".panda")) {
256 fileName.append(".panda");
257 }
258
259 scene->icRegistry()->extractToFile(blobName, fileName);
260 m_host.palette()->updateICList(m_host.icListFile());
261 m_host.palette()->updateEmbeddedICList(scene);
262 m_host.showStatusMessage(tr("IC extracted to %1").arg(fileName), 4000);
263}
264
265void ICController::embedICByFile(const QString &fileName)
266{
267 auto *tab = m_host.currentTab();
268 if (!tab) {
269 return;
270 }
271
272 auto *scene = tab->scene();
273 if (!ensureProjectSaved(scene)) {
274 return;
275 }
276 const QString contextDir = scene->contextDir();
277
278 const QString absolutePath = QDir(contextDir).absoluteFilePath(fileName);
279 QFile file(absolutePath);
280 if (!file.open(QIODevice::ReadOnly)) {
281 QMessageBox::warning(m_host.widget(), tr("Error"), tr("Could not read IC file: %1").arg(file.errorString()));
282 return;
283 }
284 QByteArray fileBytes = file.readAll();
285 file.close();
286
287 QString blobName = resolveUniqueBlobName(QFileInfo(absolutePath).baseName(), scene);
288 if (blobName.isEmpty()) {
289 return;
290 }
291
292 auto *reg = scene->icRegistry();
293 if (reg->embedICsByFile(absolutePath, fileBytes, blobName) == 0) {
294 // No existing instances — register the blob only; don't add to scene.
295 scene->receiveCommand(new RegisterBlobCommand(blobName, fileBytes, scene));
296 }
297
298 m_host.palette()->updateEmbeddedICList(scene);
299 m_host.showStatusMessage(tr("IC embedded successfully."), 4000);
300}
301
302void ICController::extractICByBlobName(const QString &blobName)
303{
304 auto *tab = m_host.currentTab();
305 if (!tab) {
306 return;
307 }
308
309 auto *scene = tab->scene();
310 if (!ensureProjectSaved(scene)) {
311 return;
312 }
313 const QString contextDir = scene->contextDir();
314
315 // Find any embedded IC with this blobName to get the blob data
316 auto *reg = scene->icRegistry();
317 if (!reg->hasBlob(blobName)) {
318 return;
319 }
320
321 const QString suggestion = QDir(contextDir).absoluteFilePath(blobName + ".panda");
322 QString fileName = FileDialogs::provider()->getSaveFileName(m_host.widget(), tr("Extract IC to file..."), suggestion, tr("Panda files") + " (*.panda)").fileName;
323
324 if (fileName.isEmpty()) {
325 return;
326 }
327
328 if (!fileName.endsWith(".panda")) {
329 fileName.append(".panda");
330 }
331
332 reg->extractToFile(blobName, fileName);
333 m_host.palette()->updateICList(m_host.icListFile());
334 m_host.palette()->updateEmbeddedICList(scene);
335 m_host.showStatusMessage(tr("IC extracted to %1").arg(fileName), 4000);
336}
337
339{
340 sentryBreadcrumb("ic", QStringLiteral("Make self-contained"));
341 auto *tab = m_host.currentTab();
342 if (!tab) {
343 return;
344 }
345
346 auto *scene = tab->scene();
347 if (!ensureProjectSaved(scene)) {
348 return;
349 }
350 const QString contextDir = scene->contextDir();
351
352 // Collect unique file paths from all file-backed ICs
353 QStringList uniqueFiles;
354 for (auto *elm : scene->elements()) {
355 if (elm->elementType() != ElementType::IC || elm->isEmbedded()) {
356 continue;
357 }
358 const QString icFile = static_cast<IC *>(elm)->file();
359 if (!icFile.isEmpty() && !uniqueFiles.contains(icFile)) {
360 uniqueFiles.append(icFile);
361 }
362 }
363
364 if (uniqueFiles.isEmpty()) {
365 m_host.showStatusMessage(tr("No file-based ICs to embed."), 4000);
366 return;
367 }
368
369 int totalEmbedded = 0;
370 bool completed = true;
371 auto *reg = scene->icRegistry();
372
373 for (const QString &icFile : std::as_const(uniqueFiles)) {
374 const QString fullPath = QDir(contextDir).absoluteFilePath(icFile);
375 QFile file(fullPath);
376 if (!file.open(QIODevice::ReadOnly)) {
377 QMessageBox::warning(m_host.widget(), tr("Error"), tr("Could not read IC file: %1").arg(file.errorString()));
378 completed = false;
379 break;
380 }
381 QByteArray fileBytes = file.readAll();
382 file.close();
383
384 QString blobName = resolveUniqueBlobName(QFileInfo(icFile).baseName(), scene);
385 if (blobName.isEmpty()) {
386 completed = false;
387 break; // User cancelled
388 }
389
390 totalEmbedded += reg->embedICsByFile(fullPath, fileBytes, blobName);
391 }
392
393 m_host.palette()->updateEmbeddedICList(scene);
394 // Only claim the circuit is self-contained when every file-based IC was embedded. On a
395 // read error or a cancelled prompt the loop breaks early, so report the partial result
396 // honestly (or stay quiet if nothing was embedded — the error/cancel already spoke).
397 if (completed) {
398 m_host.showStatusMessage(tr("Embedded %1 IC(s). Circuit is now self-contained.").arg(totalEmbedded), 4000);
399 } else if (totalEmbedded > 0) {
400 m_host.showStatusMessage(tr("Embedded %1 IC(s); some file-based ICs remain.").arg(totalEmbedded), 4000);
401 }
402}
403
405{
406 auto *tab = m_host.currentTab();
407 if (!tab) {
408 return;
409 }
410 QString fileName = FileDialogs::provider()->getOpenFileName(m_host.widget(), tr("Select IC file to embed"), m_host.currentDir().absolutePath(), tr("Panda files") + " (*.panda)");
411 if (fileName.isEmpty()) {
412 return;
413 }
414
415 QFile file(fileName);
416 if (!file.open(QIODevice::ReadOnly)) {
417 QMessageBox::warning(m_host.widget(), tr("Error"), tr("Could not read file: %1").arg(file.errorString()));
418 return;
419 }
420 QByteArray fileBytes = file.readAll();
421 file.close();
422
423 auto *scene = tab->scene();
424 QString blobName = resolveUniqueBlobName(QFileInfo(fileName).baseName(), scene);
425 if (blobName.isEmpty()) {
426 return;
427 }
428
429 scene->receiveCommand(new RegisterBlobCommand(blobName, fileBytes, scene));
430 m_host.palette()->updateEmbeddedICList(scene);
431}
432
433void ICController::removeEmbeddedIC(const QString &blobName)
434{
435 auto *tab = m_host.currentTab();
436 if (tab) {
437 tab->removeEmbeddedIC(blobName);
438 m_host.palette()->updateEmbeddedICList(tab->scene());
439 }
440}
Custom QApplication subclass with exception handling and main-window access.
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
ElementPalette: manages the left-panel element palette and search UI.
Central enumeration types for element types, groups, and signal status.
Abstract file dialog interface for testability.
ICController: integrated-circuit embed / extract / import / removal operations.
IC definition registry with file watching and embedded blob storage.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
MainWindowHost: the application context that MainWindow's extracted controllers depend on.
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)
Circuit and waveform file serialization/deserialization utilities.
Synchronous cycle-based simulation engine with event-driven clock support.
WorkSpace widget: the complete circuit editing environment for one tab.
static void guardedSlot(const QObject *context, Body &&body) noexcept
Wraps a slot body in try/catch and reports any exception synchronously, inside the slot's own stack f...
Undo command that removes a list of items from the scene.
Definition Commands.h:98
virtual FileDialogResult getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
virtual QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
void makeSelfContained()
Embeds every file-based IC in the circuit so it becomes self-contained.
void embedICByFile(const QString &fileName)
Embeds the file-based IC named fileName (drag-and-drop target).
void extractICByBlobName(const QString &blobName)
Extracts the embedded IC blob blobName to a user-chosen .panda file.
void addEmbeddedICFromFile()
Prompts for a .panda file and registers it as an embedded IC blob.
void removeICFile(const QString &icFileName)
Deletes icFileName from the project directory and any instances of it in the scene.
void addICFromFile()
Copies a chosen .panda file (and its dependencies) into the project's IC directory.
void embedSelectedIC()
Embeds the currently selected file-backed IC as a blob in the circuit.
void removeEmbeddedIC(const QString &blobName)
Removes the embedded IC blob blobName from the current circuit.
void showRemoveICHint()
Shows the "drag here to remove" hint for the file-based IC trash button.
void extractSelectedIC()
Extracts the currently selected embedded IC back out to a .panda file.
ICController(MainWindowHost &host, QObject *parent=nullptr)
int embedICsByFile(const QString &fileName, const QByteArray &fileBytes, const QString &blobName)
Converts all file-backed IC elements referencing fileName to embedded ICs using blobName.
QString uniqueBlobName(const QString &baseName) const
Returns baseName if available, or appends a numeric suffix to avoid collision.
int extractToFile(const QString &blobName, const QString &filePath)
Writes the blob to disk and converts all embedded ICs with blobName to file-backed.
Graphic element representing an Integrated Circuit (sub-circuit) box.
Definition IC.h:31
Interface MainWindow provides to its extracted controllers (export, IC, …).
virtual WorkSpace * currentTab() const =0
Currently visible WorkSpace tab, or nullptr when none is open.
virtual QWidget * widget()=0
Widget used to parent modal dialogs spawned by a controller.
Undo command that registers/unregisters a blob in the IC registry.
Definition Commands.h:450
Main circuit editing scene.
Definition Scene.h:56
const QVector< GraphicElement * > elements() const
Returns all graphic elements in the scene.
Definition Scene.cpp:336
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
const QList< GraphicElement * > selectedElements() const
Returns the list of currently selected graphic elements.
Definition Scene.cpp:427
QString contextDir() const override
Returns the directory of the .panda file associated with this scene.
Definition Scene.h:352
ICRegistry * icRegistry()
Returns the IC definition registry for this scene.
Definition Scene.h:359
static void copyPandaFile(const QFileInfo &srcPath, const QFileInfo &destPath, QSet< QString > *visited=nullptr, int depth=0)
Copies a .panda file and its file-backed IC dependencies to destPath.
Scene * scene()
Returns the Scene embedded in this workspace.
FileDialogProvider * provider()
Returns the active provider. Never null.
bool filesHaveSameContent(const QFileInfo &a, const QFileInfo &b)
Definition FileUtils.h:45
QString fileName
Selected file path, empty if cancelled.