wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
WorkspaceManager.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 <QFile>
7#include <QFileDialog>
8#include <QMessageBox>
9#include <QPushButton>
10#include <QEvent>
11#include <QMouseEvent>
12#include <QTabBar>
13#include <QTabWidget>
14#include <QUndoStack>
15
17#include "App/Core/Common.h"
19#include "App/Core/Settings.h"
21#include "App/Scene/Scene.h"
22#include "App/Scene/Workspace.h"
26
27WorkspaceManager::WorkspaceManager(QTabWidget *tab, MainWindowHost &host, QObject *parent)
28 : QObject(parent)
29 , m_tab(tab)
30 , m_host(host)
31{
32 // Watch the tab bar so a middle-click closes the tab under the cursor. Cache the tab bar
33 // pointer: the filter must never call back through m_tab, because a teardown event can reach
34 // it while m_tab is mid-destruction (its dynamic type already demoted below QTabWidget).
35 m_tabBar = m_tab->tabBar();
36 m_tabBar->installEventFilter(this);
37}
38
39bool WorkspaceManager::eventFilter(QObject *watched, QEvent *event)
40{
41 if (watched == m_tabBar && event->type() == QEvent::MouseButtonRelease) {
42 const auto *mouseEvent = static_cast<QMouseEvent *>(event);
43 if (mouseEvent->button() == Qt::MiddleButton) {
44 const int index = m_tabBar->tabAt(mouseEvent->pos());
45 if (index >= 0) {
46 closeTab(index);
47 return true;
48 }
49 }
50 }
51 return QObject::eventFilter(watched, event);
52}
53
55{
56 return m_currentTab ? m_currentTab->fileInfo() : QFileInfo();
57}
58
59QString WorkspaceManager::displayName(const WorkSpace *ws, const QFileInfo &fileInfo) const
60{
61 if (!ws) {
62 return {};
63 }
64 // Inline IC tabs use "[blobName]" — never the parent filename.
65 if (ws->isInlineIC()) {
66 return "[" + ws->inlineBlobName() + "]";
67 }
68 if (fileInfo.exists()) {
69 return fileInfo.fileName();
70 }
71 // Unsaved tab: the numbered placeholder assigned at creation (fall back for any
72 // workspace not created through createNewTab), plus a marker if it was recovered from
73 // an autosave so the user can tell it apart from a fresh, empty tab.
74 QString name = ws->untitledTitle().isEmpty() ? tr("New Project") : ws->untitledTitle();
75 if (ws->isRecovered()) {
76 name += tr(" (recovered)");
77 }
78 return name;
79}
80
82{
83 return displayName(m_currentTab, currentFile());
84}
85
87{
88 return m_currentTab ? m_currentTab->fileInfo().absoluteDir() : QDir();
89}
90
92{
93 // Walk up the parent workspace chain to find the root file on disk.
94 // Inline IC workspaces have no file of their own.
95 auto *ws = m_currentTab;
96 while (ws && ws->isInlineIC() && ws->parentWorkspace()) {
97 ws = ws->parentWorkspace();
98 }
99 if (ws) {
100 return ws->fileInfo();
101 }
102 return currentFile();
103}
104
106{
107 if (!m_currentTab) {
108 return {};
109 }
110 return m_currentTab->dolphinFileName();
111}
112
113void WorkspaceManager::setDolphinFileName(const QString &fileName)
114{
115 if (!m_currentTab) {
116 return;
117 }
118 m_currentTab->setDolphinFileName(fileName);
119}
120
121QString WorkspaceManager::nextUntitledTitle() const
122{
123 // Collect the titles other tabs currently show, minus any trailing unsaved "*".
124 QStringList taken;
125 taken.reserve(m_tab->count());
126 for (int i = 0; i < m_tab->count(); ++i) {
127 QString title = m_tab->tabText(i);
128 if (title.endsWith(QLatin1Char('*'))) {
129 title.chop(1);
130 }
131 taken.append(title);
132 }
133
134 const QString base = tr("New Project");
135 if (!taken.contains(base)) {
136 return base;
137 }
138 for (int n = 2;; ++n) {
139 const QString candidate = tr("New Project %1").arg(n);
140 if (!taken.contains(candidate)) {
141 return candidate;
142 }
143 }
144}
145
147{
148 qCDebug(zero) << "Creating new workspace.";
149 auto *workspace = new WorkSpace(m_host.widget());
150
151 connect(workspace, &WorkSpace::fileChanged, this, &WorkspaceManager::setCurrentFile);
152 connect(workspace->scene()->icRegistry(), &ICRegistry::blobRenamed, this, &WorkspaceManager::onBlobRenamed);
153
154 workspace->scene()->updateTheme();
155
156 // Number the placeholder title so several unsaved tabs stay distinguishable.
157 const QString untitledTitle = nextUntitledTitle();
158 workspace->setUntitledTitle(untitledTitle);
159
160 qCDebug(zero) << "Adding tab. #tabs: " << m_tab->count() << ", current tab: " << m_tabIndex;
161 m_tab->addTab(workspace, untitledTitle);
162 sentryBreadcrumb("ui", QStringLiteral("Tab opened"));
163
164 qCDebug(zero) << "Selecting the newly created tab.";
165 // The view's fast-mode state is applied when SceneUiBinder binds this tab below.
166 m_tab->setCurrentIndex(m_tab->count() - 1);
167}
168
169QString WorkspaceManager::promptSavePath(const QString &fileName)
170{
171 QString resolved = fileName.isEmpty() ? currentFile().absoluteFilePath() : fileName;
172
173 // If saving to an autosave path or no path at all, prompt the user for a real filename.
174 const QStringList autosaves = Settings::autosaveFiles();
175 if ((resolved.isEmpty() || autosaves.contains(resolved)) && currentFile().fileName().isEmpty()) {
176 const QString path = resolved.isEmpty() ? currentFile().absolutePath() : QFileInfo(resolved).absolutePath();
177 resolved = FileDialogs::provider()->getSaveFileName(m_host.widget(), tr("Save File"), path, tr("Panda files") + " (*.panda)").fileName;
178 }
179
180 if (resolved.isEmpty()) {
181 return {};
182 }
183
184 if (!resolved.endsWith(".panda")) {
185 resolved.append(".panda");
186 }
187
188 return resolved;
189}
190
191void WorkspaceManager::save(const QString &fileName)
192{
193 if (!m_currentTab) {
194 return;
195 }
196
197 if (m_currentTab->isInlineIC()) {
198 // No path needed: WorkSpace::save() serializes to a blob and emits a signal instead.
199 m_currentTab->save(fileName);
200 m_host.palette()->updateICList(icListFile());
201 m_host.showStatusMessage(tr("File saved successfully."), 4000);
202 return;
203 }
204
205 // Mirror the pre-refactor WorkSpace::save() bookkeeping: if the path we're about to
206 // resolve is itself a currently-tracked autosave-recovery record, forget that record
207 // once the save to the real, resolved path succeeds.
208 const QString originalFileName = fileName.isEmpty() ? currentFile().absoluteFilePath() : fileName;
209 const bool wasAutosaveRecord = !originalFileName.isEmpty() && Settings::autosaveFiles().contains(originalFileName);
210
211 QString resolvedFileName = promptSavePath(fileName);
212 if (resolvedFileName.isEmpty()) {
213 return; // Brand-new project with nothing to resolve to, or the user cancelled the dialog.
214 }
215
216 for (;;) {
217 const auto outcome = m_currentTab->save(resolvedFileName);
218 if (outcome == WorkSpace::SaveOutcome::Saved) {
219 break;
220 }
221
222 // ReadOnlyTarget: WorkSpace::save() only returns this in interactive mode (it
223 // throws instead otherwise, since there's no one to dismiss a dialog in a
224 // headless/CLI/MCP context), so prompting here is always safe. OneDrive lock,
225 // ZIP-extracted folder, network drive, write-protected attribute -- re-prompt for
226 // a writable location instead of leaving the user stuck on a modal error.
227 const QString newPath = FileDialogs::provider()->getSaveFileName(
228 m_host.widget(), tr("Save File (original location is read-only)"),
229 QFileInfo(resolvedFileName).fileName(),
230 tr("Panda files") + " (*.panda)").fileName;
231 if (newPath.isEmpty()) {
232 return;
233 }
234 resolvedFileName = newPath.endsWith(".panda") ? newPath : newPath + ".panda";
235 }
236
237 if (wasAutosaveRecord) {
238 QStringList autosaves = Settings::autosaveFiles();
239 autosaves.removeAll(originalFileName);
241 }
242
243 m_host.palette()->updateICList(icListFile());
244 m_host.showStatusMessage(tr("File saved successfully."), 4000);
245}
246
247int WorkspaceManager::closeTabAnyway()
248{
249 QMessageBox msgBox;
250 msgBox.setParent(m_host.widget());
251 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
252 msgBox.setText(tr("File not saved. Close tab anyway?"));
253 msgBox.setWindowModality(Qt::WindowModal);
254 msgBox.setDefaultButton(QMessageBox::No);
255 return msgBox.exec();
256}
257
258int WorkspaceManager::confirmSave(const bool multiple)
259{
260 QMessageBox msgBox;
261 msgBox.setParent(m_host.widget());
262
263 // When closing all tabs at once, offer "Yes to All" / "No to All" to avoid
264 // repeated per-file prompts.
265 if (multiple) {
266 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel);
267 } else {
268 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
269 }
270
271 const QString fileName = currentFile().fileName().isEmpty() ? tr("New Project") : currentFile().fileName();
272
273 msgBox.setText(fileName + tr(" has been modified.\nDo you want to save your changes?"));
274 msgBox.setWindowModality(Qt::WindowModal);
275 msgBox.setDefaultButton(QMessageBox::Yes);
276 return msgBox.exec();
277}
278
280{
281 Application::guardedSlot(this, [this] {
282 sentryBreadcrumb("ui", QStringLiteral("New project"));
283 createNewTab();
284 });
285}
286
287void WorkspaceManager::loadPandaFile(const QString &fileName)
288{
289 const QFileInfo newFileInfo(fileName);
290
291 for (int i = 0; i < m_tab->count(); ++i) {
292 if (auto *workspace = qobject_cast<WorkSpace *>(m_tab->widget(i))) {
293 if (workspace->fileInfo() == newFileInfo) {
294 m_tab->setCurrentIndex(i);
295 return;
296 }
297 }
298 }
299
300 createNewTab();
301 qCDebug(zero) << "Loading in editor.";
302 try {
303 m_currentTab->load(fileName);
304 } catch (...) {
305 // Drop the half-populated tab so we don't leak an empty workspace
306 // into the tab bar. Clear the undo stack first so closeTab doesn't
307 // prompt the user to save the partial load.
308 m_currentTab->scene()->undoStack()->clear();
309 closeTab(m_tabIndex);
310 throw;
311 }
312 // Tighten the scene rect to the loaded items immediately so subsequent
313 // interactions (selection, drag release) don't cause a viewport jump.
314 m_currentTab->scene()->resizeScene();
315 m_host.palette()->updateICList(icListFile());
316 m_host.palette()->updateEmbeddedICList(m_currentTab->scene());
317 m_host.showStatusMessage(tr("File loaded successfully."), 4000);
318}
319
320void WorkspaceManager::openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
321{
322 if (!m_currentTab) {
323 return;
324 }
325
326 // Check if this blob is already being edited in a tab
327 for (int i = 0; i < m_tab->count(); ++i) {
328 auto *ws = qobject_cast<WorkSpace *>(m_tab->widget(i));
329 if (ws && ws->isInlineIC() && ws->inlineBlobName() == blobName
330 && ws->parentWorkspace() == m_currentTab) {
331 m_tab->setCurrentIndex(i);
332 return;
333 }
334 }
335
336 auto *parentWorkspace = m_currentTab;
337
338 createNewTab();
339
340 m_currentTab->loadFromBlob(blob, parentWorkspace, icElementId, parentWorkspace->scene()->contextDir());
341 // Tighten the scene rect to the loaded items immediately so subsequent
342 // interactions (selection, drag release) don't cause a viewport jump.
343 m_currentTab->scene()->resizeScene();
344
345 // Hide management buttons for inline tabs (they use currentFile/currentDir which are empty)
346 m_host.setICButtonsVisible(false);
347
348 // Set tab title
349 int tabIndex = m_tab->indexOf(m_currentTab);
350 m_tab->setTabText(tabIndex, "[" + blobName + "]");
351
352 m_host.palette()->updateICList(icListFile());
353 m_host.palette()->updateEmbeddedICList(m_currentTab->scene());
354
355 // Connect child tab's save signal to parent
356 connect(m_currentTab, &WorkSpace::icBlobSaved, parentWorkspace, &WorkSpace::onChildICBlobSaved);
357}
358
359void WorkspaceManager::onBlobRenamed(const QString &oldName, const QString &newName)
360{
361 for (int i = 0; i < m_tab->count(); ++i) {
362 auto *ws = qobject_cast<WorkSpace *>(m_tab->widget(i));
363 if (ws && ws->isInlineIC() && ws->inlineBlobName() == oldName) {
364 ws->setInlineBlobName(newName);
365 m_tab->setTabText(i, "[" + newName + "]");
366 }
367 }
368}
369
371{
372 Application::guardedSlot(this, [this] {
373 sentryBreadcrumb("file", QStringLiteral("Open file dialog"));
374 #ifdef Q_OS_WASM
375 auto fileContentReady = [this](const QString &fileName, const QByteArray &fileContent) {
376 if (!fileName.isEmpty()) {
377 // Write file content to a temporary file
378 QFile file(fileName);
379 if (!file.open(QIODevice::WriteOnly)) {
380 return;
381 }
382 if (file.write(fileContent) != fileContent.size()) {
383 file.close();
384 return;
385 }
386 file.close();
387 loadPandaFile(fileName);
388 }
389 };
390 QFileDialog::getOpenFileContent("Panda files (*.panda)", fileContentReady);
391 #else
392 const QString fileName = FileDialogs::provider()->getOpenFileName(m_host.widget(), tr("Open File"), QString(), tr("Panda files") + " (*.panda)");
393
394 if (fileName.isEmpty()) {
395 return;
396 }
397
398 loadPandaFile(fileName);
399 #endif
400 });
401}
402
404{
405 Application::guardedSlot(this, [this] {
406 sentryBreadcrumb("file", QStringLiteral("Save"));
407 if (!m_currentTab) {
408 return;
409 }
410
411 // Inline IC tabs serialize to a blob and emit to parent — no file dialog needed.
412 if (m_currentTab->isInlineIC()) {
413 save(QString());
414 return;
415 }
416
417 #ifdef Q_OS_WASM
418 saveFileAs();
419 #else
420 // TODO: if current file is autosave ask for filename
421
422 // If the project has never been saved, fall through to a Save-As dialog.
423 QString fileName = currentFile().absoluteFilePath();
424
425 if (fileName.isEmpty()) {
426 fileName = FileDialogs::provider()->getSaveFileName(m_host.widget(), tr("Save File as ..."), QString(), tr("Panda files") + " (*.panda)").fileName;
427
428 if (fileName.isEmpty()) {
429 return;
430 }
431
432 if (!fileName.endsWith(".panda")) {
433 fileName.append(".panda");
434 }
435 }
436
437 if (warnIfOpenInAnotherTab(fileName)) {
438 return;
439 }
440
441 save(fileName);
442 #endif
443 });
444}
445
447{
448 Application::guardedSlot(this, [this] {
449 sentryBreadcrumb("file", QStringLiteral("Save as"));
450 if (!m_currentTab) {
451 return;
452 }
453
454 #ifdef Q_OS_WASM
455 // Save to a temporary file in the virtual FS, then offer it as a browser download.
456 const QString tmpPath = "/tmp/wiredpanda_save.panda";
457 save(tmpPath);
458
459 QFile file(tmpPath);
460 if (file.open(QIODevice::ReadOnly)) {
461 const QByteArray content = file.readAll();
462 file.close();
463
464 QString suggestedName = currentFile().fileName();
465 if (suggestedName.isEmpty()) {
466 suggestedName = "circuit.panda";
467 }
468 QFileDialog::saveFileContent(content, suggestedName);
469 }
470 #else
471 QString fileName = FileDialogs::provider()->getSaveFileName(m_host.widget(), tr("Save File as ..."), currentFile().absoluteFilePath(), tr("Panda files") + " (*.panda)").fileName;
472
473 if (fileName.isEmpty()) {
474 return;
475 }
476
477 if (!fileName.endsWith(".panda")) {
478 fileName.append(".panda");
479 }
480
481 if (warnIfOpenInAnotherTab(fileName)) {
482 return;
483 }
484
485 save(fileName);
486 #endif
487 });
488}
489
491{
492 Application::guardedSlot(this, [this] {
493 sentryBreadcrumb("file", QStringLiteral("Reload file"));
494 if (!currentFile().exists() || !m_currentTab) {
495 return;
496 }
497
498 const QString file = currentFile().absoluteFilePath();
499
500 closeTab(m_tabIndex);
501 loadPandaFile(file);
502 });
503}
504
505int WorkspaceManager::findTabWithFile(const QString &fileName) const
506{
507 const QFileInfo newFileInfo(fileName);
508 for (int i = 0; i < m_tab->count(); ++i) {
509 if (auto *workspace = qobject_cast<WorkSpace *>(m_tab->widget(i))) {
510 if (workspace != m_currentTab && workspace->fileInfo() == newFileInfo) {
511 return i;
512 }
513 }
514 }
515 return -1;
516}
517
518bool WorkspaceManager::warnIfOpenInAnotherTab(const QString &fileName)
519{
520 const int conflictTab = findTabWithFile(fileName);
521 if (conflictTab == -1) {
522 return false;
523 }
524
525 QMessageBox msgBox(QMessageBox::Warning,
526 tr("File Conflict"),
527 tr("The file \"%1\" is already open in another tab.").arg(QFileInfo(fileName).fileName()),
528 QMessageBox::Ok,
529 m_host.widget());
530 QPushButton *switchBtn = msgBox.addButton(tr("Switch to Tab"), QMessageBox::ActionRole);
531 msgBox.exec();
532 if (msgBox.clickedButton() == switchBtn) {
533 m_tab->setCurrentIndex(conflictTab);
534 }
535 return true;
536}
537
539{
540 // Close from last to first so that tab indices stay stable during iteration.
541 while (m_tab->count() != 0) {
542 if (!closeTab(m_tab->count() - 1)) {
543 return false;
544 }
545 }
546 return true;
547}
548
550{
551 const QStringList autosaves = Settings::autosaveFiles();
552
553 const auto workspaces = m_tab->findChildren<WorkSpace *>();
554
555 for (auto *workspace : workspaces) {
556 auto *scene = workspace->scene();
557 if (!scene) {
558 continue;
559 }
560
561 auto *undoStack = scene->undoStack();
562 if (!undoStack) {
563 continue;
564 }
565
566 // An un-clean undo stack means uncommitted edits since the last save.
567 if (!undoStack->isClean()) {
568 return true;
569 }
570
571 // An autosave file that is still in the list has not been explicitly
572 // saved by the user yet and should be treated as modified. The list
573 // stores absolute paths (see WorkSpace::autosave), so compare the
574 // workspace's absolute path — not its basename.
575 const QString filePath = workspace->fileInfo().absoluteFilePath();
576
577 if (!filePath.isEmpty() && autosaves.contains(filePath)) {
578 return true;
579 }
580 }
581
582 return false;
583}
584
586{
587 QStringList autosaves(Settings::autosaveFiles());
588
589 qCDebug(zero) << "All autosave files: " << autosaves;
590
591 for (auto it = autosaves.begin(); it != autosaves.end();) {
592 QFile file(*it);
593
594 if (!file.exists()) {
595 qCDebug(zero) << "Removing from config the autosave file that does not exist.";
596 it = autosaves.erase(it);
597 continue;
598 }
599
600 try {
601 loadPandaFile(*it);
602 } catch (const std::exception &e) {
604 QMessageBox::critical(nullptr, tr("Error!"), e.what());
605 }
606 qCDebug(zero) << "Removing autosave file that is corrupted.";
607 it = autosaves.erase(it);
608 continue;
609 }
610
611 // Mark the newly loaded tab so it knows it came from an autosave,
612 // causing it to prompt for a real save path on the next Ctrl+S.
613 m_currentTab->setAutosaveFile();
614 // Flag it as recovered so its title carries a "(recovered)" marker until saved.
615 m_currentTab->setRecovered(true);
616
617 ++it;
618 }
619
621}
622
623void WorkspaceManager::setCurrentFile(const QFileInfo &fileInfo)
624{
625 // Find the tab belonging to the workspace that emitted the signal.
626 // The sender may differ from m_currentTab (e.g. a parent workspace emitting
627 // fileChanged while an inline IC child tab is active).
628 auto *senderWs = qobject_cast<WorkSpace *>(sender());
629 if (!senderWs) {
630 senderWs = m_currentTab;
631 }
632 if (!senderWs) {
633 return;
634 }
635
636 const int tabIndex = m_tab->indexOf(senderWs);
637 if (tabIndex < 0) {
638 return;
639 }
640
641 QString text = displayName(senderWs, fileInfo);
642
643 // Append an asterisk to the tab title to indicate unsaved changes,
644 // following the common editor convention.
645 auto *scene = senderWs->scene();
646 if (scene) {
647 auto *undoStack = scene->undoStack();
648 if (undoStack && !undoStack->isClean()) {
649 text += "*";
650 }
651 }
652
653 m_tab->setTabText(tabIndex, text);
654
655 // Keep the window title in step when it's the visible tab that changed.
656 if (senderWs == m_currentTab) {
657 emit titleChanged();
658 }
659
660 // Only update tooltip and recent files for file-backed tabs.
661 if (!senderWs->isInlineIC()) {
662 m_tab->setTabToolTip(tabIndex, fileInfo.absoluteFilePath());
663 qCDebug(zero) << "Adding file to recent files.";
664 emit recentFileAdded(fileInfo.absoluteFilePath());
665 }
666
667 if (senderWs == m_currentTab) {
668 m_host.refreshICButtonsEnabled();
669 }
670}
671
672bool WorkspaceManager::closeTab(const int tabIndex)
673{
674 qCDebug(zero) << "Closing tab " << tabIndex + 1 << ", #tabs: " << m_tab->count();
675 // Activate the tab being closed so m_currentTab reflects the right workspace
676 // before we inspect its undo stack.
677 m_tab->setCurrentIndex(tabIndex);
678
679 qCDebug(zero) << "Checking if needs to save file.";
680
681 bool needsSave = false;
682 if (m_currentTab) {
683 auto *scene = m_currentTab->scene();
684 if (scene) {
685 auto *undoStack = scene->undoStack();
686 needsSave = undoStack && !undoStack->isClean();
687 }
688 }
689
690 if (needsSave) {
691 const int selectedButton = confirmSave(false);
692
693 if (selectedButton == QMessageBox::Cancel) {
694 return false;
695 }
696
697 if (selectedButton == QMessageBox::Yes) {
698 try {
699 save();
700 } catch (const std::exception &e) {
701 QMessageBox::critical(m_host.widget(), tr("Error"), e.what());
702
703 // If saving failed ask whether to discard and close anyway.
704 if (closeTabAnyway() == QMessageBox::No) {
705 return false;
706 }
707 }
708 }
709 }
710
711 qCDebug(zero) << "Deleting tab.";
712 m_currentTab->deleteLater();
713 sentryBreadcrumb("ui", QStringLiteral("Tab closed"));
714 m_tab->removeTab(tabIndex);
715
716 qCDebug(zero) << "Closed tab " << tabIndex << ", #tabs: " << m_tab->count() << ", current tab: " << m_tabIndex;
717
718 return true;
719}
720
722{
723 sentryBreadcrumb("ui", QStringLiteral("Tab changed to index %1").arg(newIndex));
724 m_tabIndex = newIndex;
725 m_currentTab = (newIndex == -1) ? nullptr : qobject_cast<WorkSpace *>(m_tab->currentWidget());
726 qCDebug(zero) << "Selecting tab: " << newIndex;
727 emit currentTabChanged(m_currentTab);
728}
Custom QApplication subclass with exception handling and main-window access.
Common logging utilities, the Pandaception error type, and helper macros.
#define qCDebug(category)
Definition Common.h:29
ElementPalette: manages the left-panel element palette and search UI.
Abstract file dialog interface for testability.
IC definition registry with file watching and embedded blob storage.
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)
Typed wrappers around QSettings for all application preferences.
WorkspaceManager: owns the circuit tabs and their file lifecycle.
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...
static bool interactiveMode
Definition Application.h:73
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 blobRenamed(const QString &oldName, const QString &newName)
Interface MainWindow provides to its extracted controllers (export, IC, …).
virtual QWidget * widget()=0
Widget used to parent modal dialogs spawned by a controller.
static QStringList autosaveFiles()
Definition Settings.cpp:186
static void setAutosaveFiles(const QStringList &files)
Definition Settings.cpp:191
A widget containing a complete circuit editing environment.
Definition Workspace.h:33
void icBlobSaved(int icElementId, const QByteArray &blob)
Emitted when an inline IC tab saves its blob (propagated to parent).
bool isRecovered() const
Definition Workspace.h:124
const QString & untitledTitle() const
Definition Workspace.h:119
bool isInlineIC() const
Returns true if this workspace is editing an embedded IC blob (not a file).
Definition Workspace.h:100
void fileChanged(const QFileInfo &fileInfo)
Emitted whenever the file info of this workspace changes (load/save).
const QString & inlineBlobName() const
Returns the blob name being edited (for tab title).
Definition Workspace.h:109
void onChildICBlobSaved(int icElementId, const QByteArray &blob)
Receives a saved blob from a child inline tab.
void setInlineBlobName(const QString &blobName)
Definition Workspace.h:114
WorkSpace * parentWorkspace() const
Returns the parent workspace (for inline IC tabs).
Definition Workspace.h:103
QFileInfo fileInfo() const
Returns the file info for the currently open circuit file.
void recentFileAdded(const QString &filePath)
Emitted when a file-backed tab is (re)named, to feed the recent-files list.
QString currentTabName() const
int confirmSave(bool multiple)
QFileInfo currentFile() const
bool closeTab(int tabIndex)
Closes the tab at tabIndex (prompting to save if needed). Returns false if cancelled.
void setDolphinFileName(const QString &fileName)
WorkspaceManager(QTabWidget *tab, MainWindowHost &host, QObject *parent=nullptr)
QString dolphinFileName() const
bool eventFilter(QObject *watched, QEvent *event) override
Closes the tab under the cursor when its tab bar is middle-clicked.
void openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
QDir currentDir() const
void onCurrentIndexChanged(int newIndex)
Reacts to QTabWidget::currentChanged: updates the current tab and emits currentTabChanged.
void save(const QString &fileName={})
void currentTabChanged(WorkSpace *tab)
Emitted when the active tab changes (or becomes null); the shell rebinds the chrome.
QFileInfo icListFile() const
void setCurrentFile(const QFileInfo &fileInfo)
Updates the tab title / tooltip / recent files for the workspace that emitted fileChanged.
void loadPandaFile(const QString &fileName)
FileDialogProvider * provider()
Returns the active provider. Never null.
QString fileName
Selected file path, empty if cancelled.