wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
MainWindow.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
4#include "App/UI/MainWindow.h"
5
6#include <algorithm>
7#include <functional>
8#include <utility>
9
10#ifdef Q_OS_WASM
11#include <emscripten/emscripten.h>
12#include <emscripten/html5.h>
13#endif
14
15#include <QActionGroup>
16#include <QCloseEvent>
17#include <QDebug>
18#include <QDesktopServices>
19#include <QDir>
20#include <QFile>
21#include <QFileInfo>
22#include <QIcon>
23#include <QKeySequence>
24#include <QLocale>
25#include <QLoggingCategory>
26#include <QMap>
27#include <QMessageBox>
28#include <QPixmapCache>
29#include <QPushButton>
30#include <QShortcut>
31#include <QStyle>
32#include <QTabBar>
33#include <QUrl>
34
35#ifdef Q_OS_MAC
36#include <QSvgRenderer>
37#endif
38
41#include "App/Core/Common.h"
45#include "App/Core/Settings.h"
48#include "App/Element/IC.h"
52#include "App/IO/RecentFiles.h"
53#include "App/Scene/Commands.h"
56#include "App/Scene/Workspace.h"
58#include "App/Tour/TourEngine.h"
62#include "App/UI/ICController.h"
64#include "App/UI/MainWindowUI.h"
68#include "App/Versions.h"
70
71#ifdef Q_OS_MAC
72void ensureSvgUsage() {
73 QSvgRenderer dummy; // for macdeployqt to add libqsvg.dylib
74}
75#endif
76
77#ifdef Q_OS_WASM
78const char *MainWindow::onBeforeUnload(int /*eventType*/, const void * /*reserved*/, void *userData)
79{
80 static_cast<MainWindow *>(userData)->updateSettings();
81 return nullptr;
82}
83#endif
84
85MainWindow::MainWindow(const QString &fileName, QWidget *parent)
86 : QMainWindow(parent)
87 , m_ui(std::make_unique<MainWindowUi>())
88{
89 qCDebug(zero) << "wiRedPanda Version = " APP_VERSION " OR " << AppVersion::current;
90 m_preferredContentDirForTesting = &ExerciseTourResources::preferredContentDir;
91 m_ui->setupUi(this);
92 qCDebug(zero) << "Settings fileName: " << Settings::fileName();
93
94 // Must be created before setupLanguage/setupTheme since both may call palette methods.
95 m_palette = new ElementPalette(m_ui.get(), this);
96
97 // Shared IC-hover preview, owned by this MainWindow as a Qt child.
98 m_icPreviewPopup = new ICPreviewPopup(this);
99
100 m_exportController = new ExportController(*this, this);
101 m_icController = new ICController(*this, this);
102 m_binder = new SceneUiBinder(m_ui.get(), m_palette, m_icPreviewPopup, this, this);
103 m_workspaceManager = new WorkspaceManager(m_ui->tab, *this, this);
104
105 // The manager owns the tab model and announces active-tab changes; the shell
106 // rebinds the chrome. The binder forwards scene-driven navigation back to the manager.
107 connect(m_workspaceManager, &WorkspaceManager::currentTabChanged, this, &MainWindow::onCurrentTabChanged);
108 connect(m_workspaceManager, &WorkspaceManager::titleChanged, this, &MainWindow::updateWindowTitle);
109 connect(m_binder, &SceneUiBinder::openICRequested, m_workspaceManager, &WorkspaceManager::openICInTab);
110 connect(m_binder, &SceneUiBinder::loadFileRequested, m_workspaceManager, &WorkspaceManager::loadPandaFile);
111
112 // Must be created before setupLanguage(): loading a translation can synchronously
113 // emit translationChanged(), which retranslateUi() handles by calling isActive() here.
114 m_exerciseEngine = new ExerciseEngine(this);
115 m_tourEngine = new TourEngine(this);
116
117 setupLanguage();
118 setupGeometry();
119 setupTheme();
120
121#ifdef Q_OS_WASM
122 // On WASM, closeEvent may not fire when the browser tab is closed.
123 // Register a beforeunload callback to persist window geometry.
124 emscripten_set_beforeunload_callback(this, &MainWindow::onBeforeUnload);
125#endif
126
127 qCDebug(zero) << "Setting left side menus.";
128 m_palette->populate();
129
130 qCDebug(zero) << "Loading recent file list.";
131 setupRecentFiles();
132
133 qCDebug(zero) << "Setting connections";
134 setupConnections();
135
136 qCDebug(zero) << "Checking playing simulation.";
137 // Start simulation running by default so the circuit is live on open.
138 m_ui->actionPlay->setChecked(true);
139
140 qCDebug(zero) << "Window title.";
141 setWindowTitle("wiRedPanda " APP_VERSION);
142
143 // Create shortcuts before the first tab so connectTab() can wire them up.
144 setupShortcuts();
145
146 qCDebug(zero) << "Building a new tab.";
147 createNewTab();
148
149 // Restore minimap visibility preference and apply to current tab if any. Position/size
150 // are restored by WorkSpace itself (from Settings::minimapGeometry(), applied in its
151 // first resizeEvent()) -- no push needed from here.
152 m_ui->actionShowMinimap->setChecked(Settings::minimapVisible());
154
155 qCDebug(zero) << "Opening file if not empty.";
156 if (!fileName.isEmpty()) {
157 loadPandaFile(fileName);
158 }
159
160 // 100 000 KB cache limit — large circuits with many IC pixmaps benefit from generous caching.
161 QPixmapCache::setCacheLimit(100000);
162
163 qCDebug(zero) << "Adding examples to menu";
164 setupExamplesMenu();
165 setupExercisesMenu();
166 setupToursMenu();
167}
168
169void MainWindow::setupLanguage()
170{
171 m_languageManager = new LanguageManager(this);
172 connect(m_languageManager, &LanguageManager::translationChanged, this, &MainWindow::retranslateUi);
173
174 QString language = Settings::language();
175 if (language.isEmpty()) {
176 const QString systemLang = s_testSystemLocaleNameOverride.value_or(QLocale::system().name());
177 const QString baseLang = systemLang.split('_').first();
178 qCDebug(zero) << "Auto-detected system locale:" << systemLang;
179
180 const auto available = m_languageManager->availableLanguages();
181 if (available.contains(systemLang)) {
182 language = systemLang;
183 } else if (available.contains(baseLang)) {
184 language = baseLang;
185 } else {
186 qCDebug(zero) << "No translation for" << systemLang << "or" << baseLang << ", falling back to English";
187 language = "en";
188 }
189 qCDebug(zero) << "Selected language:" << language;
190 }
191
192 m_languageManager->loadTranslation(language);
194}
195
196void MainWindow::setupGeometry()
197{
198 qCDebug(zero) << "Restoring geometry and setting zoom controls.";
199 restoreGeometry(Settings::mainWindowGeometry());
200 restoreState(Settings::mainWindowState());
201 m_ui->splitter->restoreGeometry(Settings::splitterGeometry());
202 m_ui->splitter->restoreState(Settings::splitterState());
203}
204
205void MainWindow::setupTheme()
206{
207 qCDebug(zero) << "Preparing theme and UI modes.";
208 auto *themeGroup = new QActionGroup(this);
209 for (auto *action : m_ui->menuTheme->actions()) {
210 themeGroup->addAction(action);
211 }
212 themeGroup->setExclusive(true);
213
214 connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, &MainWindow::updateTheme);
215 updateTheme();
217
218 // Restore toolbar label style from previous session.
219 m_ui->actionLabelsUnderIcons->setChecked(Settings::labelsUnderIcons());
220 m_ui->mainToolBar->setToolButtonStyle(Settings::labelsUnderIcons() ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly);
221
222 // Restore IC hover-preview visibility from previous session.
223 m_ui->actionICPreview->setChecked(!Settings::icPreviewDisabled());
224 m_ui->actionCheckForUpdates->setChecked(!Settings::updateChecksDisabled());
225}
226
227void MainWindow::setupRecentFiles()
228{
229 m_recentFiles = new RecentFiles(this);
230 connect(m_workspaceManager, &WorkspaceManager::recentFileAdded, m_recentFiles, &RecentFiles::addRecentFile);
231 connect(m_recentFiles, &RecentFiles::recentFilesUpdated, this, &MainWindow::updateRecentFileActions);
232 createRecentFileActions();
233}
234
235void MainWindow::setupExamplesMenu()
236{
237 const QString examplesPath = InstallRelativePaths::resolve(QStringLiteral("Examples"));
238
239 if (!examplesPath.isEmpty()) {
240 const auto entryList = QDir(examplesPath).entryList({"*.panda"}, QDir::Files);
241
242 for (const auto &entry : entryList) {
243 // Show a prettified title ("display-4bits-counter.panda" -> "Display 4bits
244 // Counter") but keep the real path in setData() so the lookup never depends on
245 // the (translatable, prettified) label — same pattern as the Recent Files menu.
246 QString title = QFileInfo(entry).completeBaseName();
247 title.replace(QLatin1Char('-'), QLatin1Char(' '));
248 title.replace(QLatin1Char('_'), QLatin1Char(' '));
249 QStringList words = title.split(QLatin1Char(' '), Qt::SkipEmptyParts);
250 for (QString &word : words) {
251 word[0] = word[0].toUpper();
252 }
253
254 auto *action = new QAction(words.join(QLatin1Char(' ')), this);
255 action->setData(examplesPath + "/" + entry);
256
257 connect(action, &QAction::triggered, this, [this] {
258 if (auto *senderAction = qobject_cast<QAction *>(sender())) {
259 loadPandaFile(senderAction->data().toString());
260 }
261 });
262
263 m_ui->menuExamples->addAction(action);
264 }
265 }
266
267 if (m_ui->menuExamples->isEmpty()) {
268 m_ui->menuExamples->menuAction()->setVisible(false);
269 }
270}
271
272void MainWindow::populateContentMenu(QMenu *menu, const QString &categoryKey,
273 const QString &openFolderText,
274 const QString &openFolderFailureText,
275 const QStringList &completed,
276 const std::function<void(const QString &)> &onSelect)
277{
278 menu->clear(); // deletes the QActions it owns (and their connections) — safe to rebuild every open
279
280 auto *openFolderAction = new QAction(openFolderText, menu);
281 connect(openFolderAction, &QAction::triggered, this, [this, categoryKey, openFolderFailureText] {
282 const QString dir = m_preferredContentDirForTesting(categoryKey);
283 if (dir.isEmpty()) {
284 QMessageBox::warning(this, tr("Error"), openFolderFailureText);
285 return;
286 }
287 QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
288 });
289 menu->addAction(openFolderAction);
290
291 const QIcon checkIcon = style()->standardIcon(QStyle::SP_DialogApplyButton);
292 const QIcon circleIcon = style()->standardIcon(QStyle::SP_ArrowRight);
293
294 const auto entries = ExerciseTourResources::discover(categoryKey);
295 if (!entries.isEmpty()) {
296 menu->addSeparator(); // only meaningful as a divider when there's something below it to divide from
297 }
298 for (const ExerciseTourResourceEntry &entry : entries) {
299 QString title = ExerciseTourResources::translate(entry.id + QStringLiteral(".title"), entry.title);
300 title.replace(QLatin1Char('&'), QStringLiteral("&&")); // literal '&' would otherwise be swallowed as a mnemonic marker
301 auto *action = new QAction(title, menu);
302 action->setIcon(completed.contains(entry.id) ? checkIcon : circleIcon);
303 action->setStatusTip(ExerciseTourResources::translate(entry.id + QStringLiteral(".description"), entry.description));
304 connect(action, &QAction::triggered, this, [onSelect, path = entry.path] { onSelect(path); });
305 menu->addAction(action);
306 }
307}
308
309void MainWindow::setupExercisesMenu()
310{
311 connect(m_ui->menuExercises, &QMenu::aboutToShow, this, [this] {
312 populateContentMenu(m_ui->menuExercises, "Exercises",
313 tr("Open My Exercises Folder"),
314 tr("Could not create or access a folder for custom exercises."),
315 Settings::completedExercises(),
316 [this](const QString &path) { startExercise(path); });
317 });
318}
319
320void MainWindow::setupToursMenu()
321{
322 connect(m_ui->menuTours, &QMenu::aboutToShow, this, [this] {
323 populateContentMenu(m_ui->menuTours, "Tours",
324 tr("Open My Tours Folder"),
325 tr("Could not create or access a folder for custom tours."),
326 Settings::completedTours(),
327 [this](const QString &path) { startTour(path); });
328 });
329}
330
331void MainWindow::setupShortcuts()
332{
333 // The scene-property shortcuts ( [ ] { } < > ) are owned by SceneUiBinder, which
334 // re-targets them to the active tab's scene on each switch.
335 auto *searchShortcut = new QShortcut(QKeySequence("Ctrl+F"), this);
336 connect(searchShortcut, &QShortcut::activated, m_ui->lineEditSearch, qOverload<>(&QWidget::setFocus));
337}
338
339void MainWindow::setupConnections()
340{
341 connect(m_ui->tab, &QTabWidget::currentChanged, m_workspaceManager, &WorkspaceManager::onCurrentIndexChanged);
342 connect(m_ui->tab, &QTabWidget::tabCloseRequested, m_workspaceManager, &WorkspaceManager::closeTab);
343
344 connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::on_actionAbout_triggered);
345 connect(m_ui->actionAboutQt, &QAction::triggered, this, &MainWindow::on_actionAboutQt_triggered);
346 connect(m_ui->actionAboutThisVersion, &QAction::triggered, this, &MainWindow::aboutThisVersion);
347 connect(m_ui->actionReportTranslationError,&QAction::triggered, this, &MainWindow::on_actionReportTranslationError_triggered);
348 connect(m_ui->actionChangeTrigger, &QAction::triggered, m_ui->elementEditor, &ElementEditor::changeTriggerAction);
349 connect(m_ui->actionDarkTheme, &QAction::triggered, this, &MainWindow::on_actionDarkTheme_triggered);
350 connect(m_ui->actionSystemTheme, &QAction::triggered, this, &MainWindow::on_actionSystemTheme_triggered);
351 connect(m_ui->actionExit, &QAction::triggered, this, &MainWindow::on_actionExit_triggered);
352 connect(m_ui->actionExportToArduino, &QAction::triggered, m_exportController, &ExportController::exportArduinoDialog);
353 connect(m_ui->actionExportToSystemVerilog, &QAction::triggered, m_exportController, &ExportController::exportSystemVerilogDialog);
354 connect(m_ui->actionExportToImage, &QAction::triggered, m_exportController, &ExportController::exportImageDialog);
355 connect(m_ui->actionExportToPdf, &QAction::triggered, m_exportController, &ExportController::exportPdfDialog);
356 connect(m_ui->actionFastMode, &QAction::triggered, this, &MainWindow::on_actionFastMode_triggered);
357 connect(m_ui->actionFlipHorizontally, &QAction::triggered, this, &MainWindow::on_actionFlipHorizontally_triggered);
358 connect(m_ui->actionFlipVertically, &QAction::triggered, this, &MainWindow::on_actionFlipVertically_triggered);
359 connect(m_ui->actionAlignLeft, &QAction::triggered, this, &MainWindow::on_actionAlignLeft_triggered);
360 connect(m_ui->actionAlignRight, &QAction::triggered, this, &MainWindow::on_actionAlignRight_triggered);
361 connect(m_ui->actionAlignTop, &QAction::triggered, this, &MainWindow::on_actionAlignTop_triggered);
362 connect(m_ui->actionAlignBottom, &QAction::triggered, this, &MainWindow::on_actionAlignBottom_triggered);
363 connect(m_ui->actionAlignHorizontalCenter, &QAction::triggered, this, &MainWindow::on_actionAlignHorizontalCenter_triggered);
364 connect(m_ui->actionAlignVerticalCenter, &QAction::triggered, this, &MainWindow::on_actionAlignVerticalCenter_triggered);
365 connect(m_ui->actionDistributeHorizontally, &QAction::triggered, this, &MainWindow::on_actionDistributeHorizontally_triggered);
366 connect(m_ui->actionDistributeVertically, &QAction::triggered, this, &MainWindow::on_actionDistributeVertically_triggered);
367 connect(m_ui->actionFullscreen, &QAction::triggered, this, &MainWindow::on_actionFullscreen_triggered);
368 connect(m_ui->actionGates, &QAction::triggered, this, &MainWindow::on_actionGates_triggered);
369 connect(m_ui->actionLabelsUnderIcons, &QAction::triggered, this, &MainWindow::on_actionLabelsUnderIcons_triggered);
370 connect(m_ui->actionICPreview, &QAction::triggered, this, &MainWindow::on_actionICPreview_triggered);
371 connect(m_ui->actionCheckForUpdates, &QAction::triggered, this, &MainWindow::on_actionCheckForUpdates_triggered);
372 connect(m_ui->actionShowMinimap, &QAction::triggered, this, &MainWindow::on_actionShowMinimap_triggered);
373 connect(m_ui->actionLightTheme, &QAction::triggered, this, &MainWindow::on_actionLightTheme_triggered);
374 connect(m_ui->actionMute, &QAction::triggered, this, &MainWindow::on_actionMute_triggered);
375 connect(m_ui->actionNew, &QAction::triggered, m_workspaceManager, &WorkspaceManager::newTab);
376 connect(m_ui->actionOpen, &QAction::triggered, m_workspaceManager, &WorkspaceManager::openFile);
377 connect(m_ui->actionPlay, &QAction::toggled, this, &MainWindow::on_actionPlay_toggled);
378 connect(m_ui->actionReloadFile, &QAction::triggered, m_workspaceManager, &WorkspaceManager::reloadFile);
379 connect(m_ui->actionRename, &QAction::triggered, m_ui->elementEditor, &ElementEditor::renameAction);
380
381 // ElementEditor IC sub-circuit actions
382 connect(m_ui->elementEditor, &ElementEditor::editSubcircuitRequested, this, [this](const QString &blobName, int icElementId) {
383 if (currentTab()) {
384 openICInTab(blobName, icElementId, currentTab()->scene()->icRegistry()->blob(blobName));
385 }
386 });
387 connect(m_ui->elementEditor, &ElementEditor::openSubcircuitFileRequested, this, &MainWindow::loadPandaFile);
388 connect(m_ui->elementEditor, &ElementEditor::embedSubcircuitRequested, m_icController, &ICController::embedSelectedIC);
389 connect(m_ui->elementEditor, &ElementEditor::extractToFileRequested, m_icController, &ICController::extractSelectedIC);
390 connect(m_ui->actionResetZoom, &QAction::triggered, this, &MainWindow::on_actionResetZoom_triggered);
391 connect(m_ui->actionZoomToFit, &QAction::triggered, this, &MainWindow::on_actionZoomToFit_triggered);
392 connect(m_ui->actionRestart, &QAction::triggered, this, &MainWindow::on_actionRestart_triggered);
393 connect(m_ui->actionRotateLeft, &QAction::triggered, this, &MainWindow::on_actionRotateLeft_triggered);
394 connect(m_ui->actionRotateRight, &QAction::triggered, this, &MainWindow::on_actionRotateRight_triggered);
395 connect(m_ui->actionSave, &QAction::triggered, m_workspaceManager, &WorkspaceManager::saveFile);
396 connect(m_ui->actionSaveAs, &QAction::triggered, m_workspaceManager, &WorkspaceManager::saveFileAs);
397 connect(m_ui->actionSelectAll, &QAction::triggered, this, &MainWindow::on_actionSelectAll_triggered);
398 connect(m_ui->actionShortcutsAndTips, &QAction::triggered, this, &MainWindow::on_actionShortcuts_and_Tips_triggered);
399 connect(m_ui->actionWaveform, &QAction::triggered, this, &MainWindow::on_actionWaveform_triggered);
400 connect(m_ui->actionWires, &QAction::triggered, this, &MainWindow::on_actionWires_triggered);
401 connect(m_ui->actionZoomIn, &QAction::triggered, this, &MainWindow::on_actionZoomIn_triggered);
402 connect(m_ui->actionZoomOut, &QAction::triggered, this, &MainWindow::on_actionZoomOut_triggered);
403 connect(m_palette, &ElementPalette::addElementRequested, this, [this](QMimeData *mimeData) {
404 auto *tab = currentTab();
405 if (!tab) {
406 delete mimeData; // no scene to take ownership; don't leak the payload
407 return;
408 }
409 // Land the element at the centre of what the user is currently viewing, not the
410 // scene origin, so it's visible however the canvas is scrolled or zoomed.
411 auto *view = tab->view();
412 const QPointF center = view->mapToScene(view->viewport()->rect().center());
413 tab->scene()->addItem(mimeData, center);
414 });
415 connect(m_ui->pushButtonAddIC, &QPushButton::clicked, m_icController, &ICController::addICFromFile);
416 connect(m_ui->pushButtonRemoveIC, &QPushButton::clicked, m_icController, &ICController::showRemoveICHint);
417 // Guarded here rather than inside removeICFile(): a failed moveToTrash() must keep
418 // throwing to direct callers, which TestICController pins, while an exception crossing
419 // signal-slot dispatch must not depend on notify()'s catch, which is unreachable on
420 // macOS and does not catch on Debian's armhf, hppa or sparc64 either (issue #525).
421 connect(m_ui->pushButtonRemoveIC, &TrashButton::removeICFile, m_icController,
422 [this](const QString &icFileName) {
423 Application::guardedSlot(m_icController, [this, &icFileName] {
424 m_icController->removeICFile(icFileName);
425 });
426 });
427 connect(m_ui->pushButtonMakeSelfContained, &QPushButton::clicked, m_icController, &ICController::makeSelfContained);
428 connect(m_ui->actionMakeSelfContained, &QAction::triggered, m_icController, &ICController::makeSelfContained);
429
430 // ICDropZone cross-section drag-and-drop
431 connect(m_ui->dropZoneFileBased, &ICDropZone::extractByBlobNameRequested, m_icController, &ICController::extractICByBlobName);
432 connect(m_ui->dropZoneEmbedded, &ICDropZone::embedByFileRequested, m_icController, &ICController::embedICByFile);
433
434 // Embedded IC section buttons
435 connect(m_ui->pushButtonAddEmbeddedIC, &QPushButton::clicked, m_icController, &ICController::addEmbeddedICFromFile);
436 connect(m_ui->pushButtonRemoveEmbeddedIC, &QPushButton::clicked, m_icController, &ICController::showRemoveICHint);
437 connect(m_ui->pushButtonRemoveEmbeddedIC, &TrashButton::removeEmbeddedIC, m_icController, &ICController::removeEmbeddedIC);
438
439 // These edit actions always delegate to the current tab's scene, so they
440 // never need to be rewired on tab switch.
441 connectSceneAction(m_ui->actionClearSelection, &Scene::clearSelection);
442 connectSceneAction(m_ui->actionCopy, &Scene::copyAction);
443 connectSceneAction(m_ui->actionCut, &Scene::cutAction);
444 connectSceneAction(m_ui->actionDelete, &Scene::deleteAction);
445 connectSceneAction(m_ui->actionPaste, &Scene::pasteAction);
446 connectSceneAction(m_ui->actionDuplicate, &Scene::duplicateAction);
447}
448
449void MainWindow::connectSceneAction(QAction *action, void (Scene::*method)())
450{
451 connect(action, &QAction::triggered, this, [this, method] {
452 if (currentTab()) {
453 (currentTab()->scene()->*method)();
454 }
455 });
456}
457
459{
460 // Tear down the active tab's chrome wiring before child objects are destroyed.
461 m_binder->unbind();
462
463 // Same ordering concern, one level deeper: the waveform window is a child, so ~QWidget
464 // deletes it and its destroyed() reaches the slot below with this object already torn
465 // down to its QWidget base. Sever it here, while the derived object is still whole.
466 disconnect(m_bwdDestroyed);
467}
468
470{
471 m_workspaceManager->createNewTab();
472}
473
474void MainWindow::setFastMode(const bool fastMode)
475{
476 m_ui->actionFastMode->setChecked(fastMode);
477
478 if (currentTab()) {
479 currentTab()->view()->setFastMode(fastMode);
480 }
481}
482
483void MainWindow::on_actionExit_triggered()
484{
485 Application::guardedSlot(this, [this] {
486 close();
487 });
488}
489
490void MainWindow::save(const QString &fileName)
491{
492 m_workspaceManager->save(fileName);
493}
494
496{
497 QMainWindow::show();
498
499 qCDebug(zero) << "Checking for autosave file recovery.";
500 m_workspaceManager->loadAutosaveFiles();
501
502 auto *updateController = new UpdateController(this);
503 updateController->checkForUpdates();
504
505 // First-ever launch: auto-start the built-in UI walkthrough. Gated on interactiveMode so
506 // it doesn't fire in the CLI/MCP-automation paths that also construct a MainWindow.
509 startTour(QStringLiteral(":/Tours/ui-overview.json"));
510 }
511}
512
513void MainWindow::aboutThisVersion()
514{
515 QMessageBox msgBox;
516 msgBox.setParent(this);
517 msgBox.setStandardButtons(QMessageBox::Ok);
518 msgBox.setIcon(QMessageBox::Icon::Information);
519 msgBox.setWindowTitle("wiRedPanda " APP_VERSION);
520 msgBox.setText(
521 tr("wiRedPanda %1\n\n"
522 "This version includes automatic migration of older project files.\n"
523 "When you open a project file older than the current version, it will be automatically "
524 "upgraded to the current format and a versioned backup will be created.\n\n"
525 "To open projects containing ICs (or boxes), appearances, and/or beWavedDolphin simulations, "
526 "their files must be in the same directory as the main project file.\n"
527 "wiRedPanda %1 will automatically list all other .panda files located "
528 "in the same directory as the current project as ICs in the editor tab.\n"
529 "You have to save new projects before accessing ICs and appearances, or running "
530 "beWavedDolphin simulations.").arg(APP_VERSION));
531 msgBox.setWindowModality(Qt::WindowModal);
532 msgBox.setDefaultButton(QMessageBox::Ok);
533
534 msgBox.exec();
535}
536
537void MainWindow::on_actionWires_triggered(const bool checked)
538{
539 Application::guardedSlot(this, [this, checked] {
540 sentryBreadcrumb("ui", QStringLiteral("Wires: %1").arg(checked));
541 if (currentTab()) {
542 currentTab()->scene()->showWires(checked);
543 }
544 });
545}
546
547void MainWindow::on_actionRotateRight_triggered()
548{
549 Application::guardedSlot(this, [this] {
550 if (currentTab()) {
552 }
553 });
554}
555
556void MainWindow::on_actionRotateLeft_triggered()
557{
558 Application::guardedSlot(this, [this] {
559 if (currentTab()) {
561 }
562 });
563}
564
565void MainWindow::loadPandaFile(const QString &fileName)
566{
567 m_workspaceManager->loadPandaFile(fileName);
568}
569
570void MainWindow::openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
571{
572 m_workspaceManager->openICInTab(blobName, icElementId, blob);
573}
574
575void MainWindow::on_actionAbout_triggered()
576{
577 Application::guardedSlot(this, [this] {
578 QMessageBox::about(
579 this,
580 "wiRedPanda",
581 tr("<p>wiRedPanda is software developed by students of the Federal University of São Paulo"
582 " to help students learn about logic circuits.</p>"
583 "<p>Software version: %1</p>"
584 "<p><strong>Creators:</strong></p>"
585 "<ul>"
586 "<li> Davi Morales </li>"
587 "<li> Lucas Lellis </li>"
588 "<li> Rodrigo Torres </li>"
589 "<li> Prof. Fábio Cappabianco, Ph.D. </li>"
590 "</ul>"
591 "<p> wiRedPanda is currently maintained by Prof. Fábio Cappabianco, Ph.D., João Pedro M. Oliveira, Matheus R. Esteves and Maycon A. Santana.</p>"
592 "<p> Please file a report at our GitHub page if you find a bug or want to request a new feature.</p>"
593 "<p><a href=\"https://gibis-unifesp.github.io/wiRedPanda/\">Visit our website!</a></p>")
594 .arg(QApplication::applicationVersion()));
595 });
596}
597
599{
600 // Keyed by label so rows sort alphabetically and any duplicate collapses.
601 QMap<QString, QString> byLabel;
602
603 // Undo/Redo belong to the active Scene's stack, not this window, so findChildren()
604 // can't see them; add them explicitly with the platform-standard chords.
605 byLabel.insert(tr("Redo"), QKeySequence(QKeySequence::Redo).toString(QKeySequence::NativeText));
606 byLabel.insert(tr("Undo"), QKeySequence(QKeySequence::Undo).toString(QKeySequence::NativeText));
607
608 const auto actions = findChildren<QAction *>();
609 for (const auto *action : actions) {
610 const QKeySequence seq = action->shortcut();
611 if (seq.isEmpty()) {
612 continue;
613 }
614 QString label = action->text();
615 label.remove(QLatin1Char('&')); // strip menu mnemonics
616 if (label.endsWith(QLatin1String("..."))) {
617 label.chop(3);
618 }
619 label = label.trimmed();
620 if (!label.isEmpty()) {
621 byLabel.insert(label, seq.toString(QKeySequence::NativeText));
622 }
623 }
624
625 QString rows;
626 for (auto it = byLabel.cbegin(); it != byLabel.cend(); ++it) {
627 rows += QStringLiteral("<tr><td><b>%1</b>&nbsp;&nbsp;&nbsp;</td><td>%2</td></tr>")
628 .arg(it.value().toHtmlEscaped(), it.key().toHtmlEscaped());
629 }
630
631 return tr("<h1>Keyboard Shortcuts</h1>"
632 "<table>%1</table>"
633 "<h1>Element Property Navigation</h1>"
634 "<ul style=\"list-style:none;\">"
635 "<li> [ / ] : Previous / next primary property </li>"
636 "<li> { / } : Previous / next secondary property </li>"
637 "<li> &lt; / &gt; : Morph to previous / next element </li>"
638 "</ul>"
639 "<h1>General Tips</h1>"
640 "<ul style=\"list-style:none;\">"
641 "<li> Double-click a wire to create a node </li>"
642 "<li> Drag an element from the left panel onto the canvas to add it </li>"
643 "<li> Nudge the selection with the arrow keys (hold Shift for larger steps) </li>"
644 "<li> Drop a .panda file onto the canvas to open it </li>"
645 "</ul>")
646 .arg(rows);
647}
648
649void MainWindow::on_actionShortcuts_and_Tips_triggered()
650{
651 Application::guardedSlot(this, [this] {
652 QMessageBox::information(this, tr("Shortcuts and Tips"), shortcutsHelpHtml());
653 });
654}
655
656void MainWindow::on_actionAboutQt_triggered()
657{
658 Application::guardedSlot(this, [this] {
659 QMessageBox::aboutQt(this);
660 });
661}
662
663void MainWindow::on_actionReportTranslationError_triggered()
664{
665 Application::guardedSlot(this, [] {
666 QDesktopServices::openUrl(QUrl("https://hosted.weblate.org/projects/wiredpanda/wiredpanda"));
667 });
668}
669
671{
672 bool closeWindow = false;
673
674 // If nothing is modified, ask once before exiting so the user can't
675 // accidentally quit with a keyboard shortcut. If there are unsaved changes,
676 // delegate to closeFiles() which prompts per-tab.
677 if (!m_workspaceManager->hasModifiedFiles()) {
678 auto reply =
679 QMessageBox::question(
680 this,
681 tr("Exit") + " " + QApplication::applicationName(),
682 tr("Are you sure?"),
683 QMessageBox::Cancel | QMessageBox::Yes,
684 QMessageBox::Yes);
685
686 if (reply == QMessageBox::Yes) {
687 closeWindow = true;
688 }
689 } else if (m_workspaceManager->closeFiles()) {
690 closeWindow = true;
691 }
692
693 if (closeWindow) {
694 // Persist window/splitter layout so the next session opens the same way.
695 updateSettings();
696 event->accept();
697 } else {
698 event->ignore();
699 }
700}
701
702void MainWindow::updateSettings()
703{
704 Settings::setMainWindowGeometry(saveGeometry());
705 Settings::setMainWindowState(saveState());
706 Settings::setSplitterGeometry(m_ui->splitter->saveGeometry());
707 Settings::setSplitterState(m_ui->splitter->saveState());
708}
709
711{
712 m_ui->pushButtonAddIC->setVisible(visible);
713 m_ui->pushButtonRemoveIC->setVisible(visible);
714 m_ui->pushButtonMakeSelfContained->setVisible(visible);
715}
716
718{
719 // Add IC needs a real project directory to copy the chosen .panda into;
720 // gating click-ability on a saved file avoids the "Save file first."
721 // throw → modal-error UX dead end.
722 const bool hasFile = currentTab() && currentTab()->fileInfo().isReadable();
723 m_ui->pushButtonAddIC->setEnabled(hasFile);
724}
725
726QFileInfo MainWindow::currentFile() const
727{
728 return m_workspaceManager->currentFile();
729}
730
732{
733 return m_workspaceManager->hasModifiedFiles();
734}
735
737{
738 return m_workspaceManager->currentDir();
739}
740
742{
743 return this;
744}
745
747{
748 return this;
749}
750
752{
753 return m_palette;
754}
755
757{
758 m_workspaceManager->saveFile();
759}
760
761void MainWindow::showStatusMessage(const QString &message, int timeout)
762{
763 m_ui->statusBar->showMessage(message, timeout);
764}
765
766QFileInfo MainWindow::icListFile() const
767{
768 return m_workspaceManager->icListFile();
769}
770
771void MainWindow::on_actionSelectAll_triggered()
772{
773 Application::guardedSlot(this, [this] {
774 sentryBreadcrumb("ui", QStringLiteral("Select all"));
775 if (!currentTab()) {
776 return;
777 }
778
779 currentTab()->scene()->selectAll();
780 });
781}
782
784{
785 return m_workspaceManager->currentTab();
786}
787
788void MainWindow::onCurrentTabChanged(WorkSpace *newTab)
789{
790 // Reaction to WorkspaceManager::currentTabChanged: rebind the shared chrome to the
791 // new scene and refresh the tab-navigation view state (file-based IC list, buttons).
792 // currentTab() already reflects newTab here (WorkspaceManager updates its current-tab
793 // field before emitting this signal), so the tab actually being left is tracked
794 // separately in m_previousTab rather than read via currentTab().
795 WorkSpace *prevTab = m_previousTab;
796 m_previousTab = newTab;
797
798 m_binder->unbind(); // tear down the previously bound tab's chrome wiring
799 // Hide the editor panel during the transition; SceneUiBinder::bind restores it
800 // once the new scene's selection is known.
801 m_ui->elementEditor->hide();
802
803 // Detach exercise overlay from the leaving tab
804 if (prevTab) {
805 prevTab->setExerciseOverlay(nullptr);
806 }
807 if (m_exerciseOverlay && m_exerciseEngine && m_exerciseEngine->isActive()) {
808 m_exerciseOverlay->hide();
809 m_exerciseOverlay->setParent(nullptr);
810 }
811
812 if (!newTab) {
813 // All tabs were closed; reset state.
814 m_palette->updateICList(QFileInfo());
815 m_palette->updateEmbeddedICList(nullptr);
816 updateWindowTitle();
817 return;
818 }
819
820 m_binder->bind(newTab);
821 m_palette->updateICList(icListFile());
822
823 // Apply the minimap visibility preference to the newly activated tab. Position/size are
824 // per-tab (each WorkSpace restores its own from Settings::minimapGeometry() on first
825 // layout), so there's nothing to push here for those.
827
828 // Hide management buttons for inline IC tabs (they use currentFile/currentDir which are empty)
831
832 // Re-attach exercise overlay to the arriving tab
833 if (m_exerciseEngine && m_exerciseEngine->isActive() && m_exerciseOverlay) {
834 m_exerciseEngine->setScene(newTab->scene());
835 m_exerciseOverlay->setParent(newTab);
836 newTab->setExerciseOverlay(m_exerciseOverlay);
837 m_exerciseOverlay->repositionToParent();
838 m_exerciseOverlay->show();
839 m_exerciseOverlay->raise();
840 }
841
842 updateWindowTitle();
843}
844
845void MainWindow::updateWindowTitle()
846{
847 auto *tab = currentTab();
848 if (!tab) {
849 setWindowTitle(QStringLiteral("wiRedPanda " APP_VERSION));
850 setWindowModified(false);
851 return;
852 }
853
854 // "<name>[*] — wiRedPanda <version>": Qt swaps "[*]" for "*" when the window is marked
855 // modified, and drops it otherwise (a native dot on macOS).
856 setWindowTitle(tr("%1[*] — wiRedPanda %2")
857 .arg(m_workspaceManager->currentTabName(), QStringLiteral(APP_VERSION)));
858 setWindowModified(!tab->scene()->undoStack()->isClean());
859}
860
861void MainWindow::on_actionGates_triggered(const bool checked)
862{
863 Application::guardedSlot(this, [this, checked] {
864 sentryBreadcrumb("ui", QStringLiteral("Gates: %1").arg(checked));
865 if (!currentTab()) {
866 return;
867 }
868
869 // Wire visibility depends on gates being visible: if gates are hidden, wires
870 // make no sense and should be hidden too. Re-enable the wire toggle only when
871 // gates are shown so the user can't end up with floating wires.
872 m_ui->actionWires->setEnabled(checked);
873 currentTab()->scene()->showWires(checked ? m_ui->actionWires->isChecked() : checked);
874 currentTab()->scene()->showGates(checked);
875 });
876}
877
878void MainWindow::exportToArduino(QString fileName)
879{
880 m_exportController->exportToArduino(std::move(fileName));
881}
882
884{
885 m_exportController->exportToSystemVerilog(std::move(fileName));
886}
887
888void MainWindow::exportToWaveFormFile(const QString &fileName)
889{
890 m_exportController->exportToWaveFormFile(fileName);
891}
892
894{
895 m_exportController->exportToWaveFormTerminal();
896}
897
898void MainWindow::on_actionZoomIn_triggered() const
899{
900 Application::guardedSlot(this, [this] {
901 if (!currentTab()) {
902 return;
903 }
904
905 currentTab()->view()->zoomIn();
906 });
907}
908
909void MainWindow::on_actionZoomOut_triggered() const
910{
911 Application::guardedSlot(this, [this] {
912 if (!currentTab()) {
913 return;
914 }
915
916 currentTab()->view()->zoomOut();
917 });
918}
919
920void MainWindow::on_actionResetZoom_triggered() const
921{
922 Application::guardedSlot(this, [this] {
923 if (!currentTab()) {
924 return;
925 }
926
927 currentTab()->view()->resetZoom();
928 });
929}
930
931void MainWindow::on_actionZoomToFit_triggered() const
932{
933 Application::guardedSlot(this, [this] {
934 if (!currentTab()) {
935 return;
936 }
937
938 currentTab()->view()->zoomToFit();
939 });
940}
941
942void MainWindow::updateRecentFileActions()
943{
944 const auto files = m_recentFiles->recentFiles();
945 const int numRecentFiles = static_cast<int>(qMin(files.size(), RecentFiles::maxFiles));
946
947 if (numRecentFiles > 0) {
948 m_ui->menuRecentFiles->setEnabled(true);
949 }
950
951 auto actions = m_ui->menuRecentFiles->actions();
952
953 // The menu has exactly RecentFiles::maxFiles pre-allocated actions; update
954 // visible ones in order, hide the rest. Prefix "&1", "&2" … adds a mnemonic
955 // so the entries are keyboard-accessible without a mouse.
956 for (int i = 0; i < numRecentFiles; ++i) {
957 const QString text = "&" + QString::number(i + 1) + " " + QFileInfo(files.at(i)).fileName();
958 actions.at(i)->setText(text);
959 actions.at(i)->setData(files.at(i));
960 actions.at(i)->setVisible(true);
961 }
962
963 for (int i = numRecentFiles; i < RecentFiles::maxFiles; ++i) {
964 actions.at(i)->setVisible(false);
965 }
966}
967
968void MainWindow::openRecentFile()
969{
970 if (auto *action = qobject_cast<QAction *>(sender())) {
971 sentryBreadcrumb("file", QStringLiteral("Open recent file"));
972 loadPandaFile(action->data().toString());
973 }
974}
975
976void MainWindow::createRecentFileActions()
977{
978 m_ui->menuRecentFiles->clear();
979
980 for (int i = 0; i < RecentFiles::maxFiles; ++i) {
981 auto *action = new QAction(this);
982 action->setVisible(false);
983 connect(action, &QAction::triggered, this, &MainWindow::openRecentFile);
984 m_ui->menuRecentFiles->addAction(action);
985 }
986
987 updateRecentFileActions();
988}
989
991{
992 m_ui->retranslateUi();
993 m_ui->elementEditor->retranslateUi();
994 m_palette->retranslateLabels();
995
996 for (int index = 0; index < m_ui->tab->count(); ++index) {
997 auto *workspace = qobject_cast<WorkSpace *>(m_ui->tab->widget(index));
998 if (!workspace) { // LCOV_EXCL_LINE — WorkspaceManager::createNewTab()/openICInTab() are the only two call sites that ever add a widget to m_ui->tab, and both always add a real WorkSpace.
999 continue; // LCOV_EXCL_LINE — see above.
1000 }
1001 auto *scene = workspace->scene();
1002 if (!scene) { // LCOV_EXCL_LINE — WorkSpace::scene() returns &m_scene (a value member's address), never null.
1003 continue; // LCOV_EXCL_LINE — see above.
1004 }
1005 auto *undoStack = scene->undoStack();
1006 if (!undoStack) { // LCOV_EXCL_LINE — Scene::undoStack() returns &m_undoStack (a value member's address), never null.
1007 continue; // LCOV_EXCL_LINE — see above.
1008 }
1009 QString text;
1010 if (workspace->isInlineIC()) {
1011 text = "[" + workspace->inlineBlobName() + "]";
1012 } else {
1013 auto fileInfo = workspace->fileInfo();
1014 text = fileInfo.exists() ? fileInfo.fileName() : tr("New Project");
1015 }
1016
1017 if (!undoStack->isClean()) {
1018 text += "*";
1019 }
1020
1021 m_ui->tab->setTabText(index, text);
1022
1023 scene->retranslateUi();
1024
1025 for (auto *elm : workspace->scene()->elements()) {
1026 elm->retranslate();
1027 }
1028 }
1029
1030 if (m_exerciseEngine->isActive()) {
1031 m_exerciseEngine->retranslate();
1032 }
1033 if (m_tourEngine->isActive()) {
1034 m_tourEngine->retranslate();
1035 }
1036}
1037
1038void MainWindow::loadTranslation(const QString &language)
1039{
1040 m_languageManager->loadTranslation(language);
1041}
1042
1044{
1045 m_ui->menuLanguage->clear();
1046
1047 auto *languageGroup = new QActionGroup(this);
1048 languageGroup->setExclusive(true);
1049
1050 for (const QString &langCode : m_languageManager->availableLanguages()) {
1051 auto *action = new QAction(m_languageManager->displayName(langCode), this);
1052 action->setCheckable(true);
1053 action->setData(langCode);
1054 action->setIcon(QIcon(m_languageManager->flagIcon(langCode)));
1055
1056 if (langCode == Settings::language() || (langCode == "en" && Settings::language().isEmpty())) {
1057 action->setChecked(true);
1058 }
1059
1060 languageGroup->addAction(action);
1061 m_ui->menuLanguage->addAction(action);
1062
1063 connect(action, &QAction::triggered, this, [this, langCode]() {
1064 m_languageManager->loadTranslation(langCode);
1065 });
1066 }
1067}
1068
1069void MainWindow::on_actionPlay_toggled(const bool checked)
1070{
1071 sentryBreadcrumb("simulation", QStringLiteral("Play toggled: %1").arg(checked));
1072 if (!currentTab()) {
1073 return;
1074 }
1075
1076 auto *simulation = currentTab()->simulation();
1077
1078 // The action is checkable; its toggled(bool) signal drives start/stop directly.
1079 checked ? simulation->start() : simulation->stop();
1080}
1081
1082void MainWindow::on_actionRestart_triggered()
1083{
1084 Application::guardedSlot(this, [this] {
1085 sentryBreadcrumb("simulation", QStringLiteral("Simulation restart"));
1086 if (!currentTab()) {
1087 return;
1088 }
1089
1091 });
1092}
1093
1094void MainWindow::on_actionFastMode_triggered(const bool checked)
1095{
1096 Application::guardedSlot(this, [this, checked] {
1097 sentryBreadcrumb("ui", QStringLiteral("Fast mode: %1").arg(checked));
1098 setFastMode(checked);
1099 Settings::setFastMode(checked);
1100 });
1101}
1102
1103void MainWindow::on_actionWaveform_triggered()
1104{
1105 Application::guardedSlot(this, [this] {
1106 if (m_bwd) {
1107 m_bwd->raise();
1108 m_bwd->activateWindow();
1109 return;
1110 }
1111 if (!currentTab()) {
1112 return;
1113 }
1114
1115 sentryBreadcrumb("ui", QStringLiteral("Waveform dialog opened"));
1116 qCDebug(zero) << "BD fileName: " << currentTab()->dolphinFileName();
1117 auto *bwd = new BewavedDolphin(currentTab()->scene(), true, this, this);
1118 bwd->createWaveform(currentTab()->dolphinFileName());
1119 m_bwd = bwd;
1120 m_bwdDestroyed = connect(bwd, &QObject::destroyed, this, [this] {
1121 if (m_exerciseOverlay && m_exerciseEngine && m_exerciseEngine->isActive()) {
1122 m_exerciseOverlay->hide();
1123 m_exerciseOverlay->setParent(nullptr);
1124 }
1125 });
1126 bwd->show();
1127 });
1128}
1129
1130void MainWindow::on_actionLightTheme_triggered()
1131{
1132 Application::guardedSlot(qApp, [] {
1133 sentryBreadcrumb("ui", QStringLiteral("Theme: light"));
1135 });
1136}
1137
1138void MainWindow::on_actionDarkTheme_triggered()
1139{
1140 Application::guardedSlot(qApp, [] {
1141 sentryBreadcrumb("ui", QStringLiteral("Theme: dark"));
1143 });
1144}
1145
1146void MainWindow::on_actionSystemTheme_triggered()
1147{
1148 Application::guardedSlot(qApp, [] {
1149 sentryBreadcrumb("ui", QStringLiteral("Theme: system"));
1151 });
1152}
1153
1154void MainWindow::updateTheme()
1155{
1156 switch (ThemeManager::theme()) {
1157 case Theme::Dark: m_ui->actionDarkTheme->setChecked(true); break;
1158 case Theme::Light: m_ui->actionLightTheme->setChecked(true); break;
1159 case Theme::System: m_ui->actionSystemTheme->setChecked(true); break;
1160 }
1161
1162 m_palette->updateTheme();
1163 m_ui->elementEditor->updateTheme();
1164}
1165
1166void MainWindow::on_actionFlipHorizontally_triggered()
1167{
1168 Application::guardedSlot(this, [this] {
1169 if (!currentTab()) {
1170 return;
1171 }
1172
1174 });
1175}
1176
1177void MainWindow::on_actionFlipVertically_triggered()
1178{
1179 Application::guardedSlot(this, [this] {
1180 if (!currentTab()) {
1181 return;
1182 }
1183
1185 });
1186}
1187
1188void MainWindow::on_actionAlignLeft_triggered()
1189{
1190 Application::guardedSlot(this, [this] {
1191 if (!currentTab()) {
1192 return;
1193 }
1194
1195 currentTab()->scene()->alignLeft();
1196 });
1197}
1198
1199void MainWindow::on_actionAlignRight_triggered()
1200{
1201 Application::guardedSlot(this, [this] {
1202 if (!currentTab()) {
1203 return;
1204 }
1205
1206 currentTab()->scene()->alignRight();
1207 });
1208}
1209
1210void MainWindow::on_actionAlignTop_triggered()
1211{
1212 Application::guardedSlot(this, [this] {
1213 if (!currentTab()) {
1214 return;
1215 }
1216
1217 currentTab()->scene()->alignTop();
1218 });
1219}
1220
1221void MainWindow::on_actionAlignBottom_triggered()
1222{
1223 Application::guardedSlot(this, [this] {
1224 if (!currentTab()) {
1225 return;
1226 }
1227
1229 });
1230}
1231
1232void MainWindow::on_actionAlignHorizontalCenter_triggered()
1233{
1234 Application::guardedSlot(this, [this] {
1235 if (!currentTab()) {
1236 return;
1237 }
1238
1240 });
1241}
1242
1243void MainWindow::on_actionAlignVerticalCenter_triggered()
1244{
1245 Application::guardedSlot(this, [this] {
1246 if (!currentTab()) {
1247 return;
1248 }
1249
1251 });
1252}
1253
1254void MainWindow::on_actionDistributeHorizontally_triggered()
1255{
1256 Application::guardedSlot(this, [this] {
1257 if (!currentTab()) {
1258 return;
1259 }
1260
1262 });
1263}
1264
1265void MainWindow::on_actionDistributeVertically_triggered()
1266{
1267 Application::guardedSlot(this, [this] {
1268 if (!currentTab()) {
1269 return;
1270 }
1271
1273 });
1274}
1275
1277{
1278 return m_workspaceManager->dolphinFileName();
1279}
1280
1281void MainWindow::setDolphinFileName(const QString &fileName)
1282{
1283 m_workspaceManager->setDolphinFileName(fileName);
1284}
1285
1286void MainWindow::on_actionFullscreen_triggered()
1287{
1288 Application::guardedSlot(this, [this] {
1289 sentryBreadcrumb("ui", QStringLiteral("Fullscreen toggled"));
1290 isFullScreen() ? showNormal() : showFullScreen();
1291 });
1292}
1293
1294void MainWindow::on_actionMute_triggered(const bool checked)
1295{
1296 Application::guardedSlot(this, [this, checked] {
1297 sentryBreadcrumb("ui", QStringLiteral("Mute toggled"));
1298 if (!currentTab()) {
1299 return;
1300 }
1301
1302 currentTab()->simulation()->setUserMuted(checked);
1303 m_ui->actionMute->setText(checked ? tr("Unmute") : tr("Mute"));
1304 });
1305}
1306
1307void MainWindow::on_actionLabelsUnderIcons_triggered(const bool checked)
1308{
1309 Application::guardedSlot(this, [this, checked] {
1310 sentryBreadcrumb("ui", QStringLiteral("Labels under icons: %1").arg(checked));
1311 m_ui->mainToolBar->setToolButtonStyle(checked ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly);
1313 });
1314}
1315
1316void MainWindow::on_actionICPreview_triggered(const bool checked)
1317{
1318 Application::guardedSlot(this, [checked] {
1319 sentryBreadcrumb("ui", QStringLiteral("IC preview: %1").arg(checked));
1321 });
1322}
1323
1324void MainWindow::on_actionCheckForUpdates_triggered(const bool checked)
1325{
1326 Application::guardedSlot(this, [checked] {
1327 sentryBreadcrumb("ui", QStringLiteral("Auto update checks: %1").arg(checked));
1329 });
1330}
1331
1332void MainWindow::on_actionShowMinimap_triggered(const bool checked)
1333{
1334 Application::guardedSlot(this, [this, checked] {
1335 sentryBreadcrumb("ui", QStringLiteral("Show minimap: %1").arg(checked));
1337 if (currentTab()) currentTab()->setMinimapVisible(checked);
1338 });
1339}
1340
1342{
1343 switch (event->type()) {
1344 case QEvent::WindowActivate: {
1345 // Resume simulation when the window regains focus.
1346 if (m_ui->actionPlay->isChecked()) {
1347 on_actionPlay_toggled(true);
1348 }
1349 break;
1350 }
1351
1352 case QEvent::WindowDeactivate: {
1353 // Pause simulation when the window loses focus unless the user opted in
1354 // to background simulation (useful for demoing circuits on a second display).
1355 if (!m_ui->actionBackground_Simulation->isChecked()) {
1356 on_actionPlay_toggled(false);
1357 }
1358 break;
1359 }
1360
1361 default: break;
1362 };
1363
1364 return QMainWindow::event(event);
1365}
1366
1367void MainWindow::startExercise(const QString &resourcePath)
1368{
1369 if (!currentTab() || resourcePath.isEmpty()) {
1370 return;
1371 }
1372 if (!m_exerciseEngine->loadFromResource(resourcePath)) {
1373 qWarning() << "ExerciseEngine: failed to load" << resourcePath;
1374 return;
1375 }
1376
1377 if (currentTab()) {
1378 currentTab()->setExerciseOverlay(nullptr);
1379 }
1380 delete m_exerciseOverlay;
1381 m_exerciseOverlay = new ExerciseOverlay(m_exerciseEngine, currentTab());
1382
1383 // m_exerciseEngine outlives any single exercise run, so a prior startExercise() call's
1384 // click-handling connection (below) must be dropped before reconnecting, or replaying an
1385 // exercise fires accumulated handlers once per past run.
1386 disconnect(m_exerciseEngine, &ExerciseEngine::stepChanged, this, nullptr);
1387
1388 // Must precede stepChanged→onStepChanged so actions run before the overlay updates.
1389 connect(m_exerciseEngine, &ExerciseEngine::stepChanged, this,
1390 [this](int, int, const ExerciseStep &step) {
1391 for (const QString &id : step.click) clickTarget(id);
1392
1393 if (!m_exerciseOverlay) {
1394 return;
1395 }
1396 const bool wantBwd = (step.context == "bwd");
1397 if (wantBwd && m_bwd) {
1398 m_exerciseOverlay->setParent(m_bwd->centralWidget());
1399 m_bwd->setExerciseOverlay(m_exerciseOverlay);
1400 if (currentTab()) {
1401 currentTab()->setExerciseOverlay(nullptr);
1402 }
1403 } else if (!wantBwd && currentTab()) {
1404 m_exerciseOverlay->setParent(currentTab());
1405 currentTab()->setExerciseOverlay(m_exerciseOverlay);
1406 if (m_bwd) {
1407 m_bwd->setExerciseOverlay(nullptr);
1408 }
1409 }
1410 m_exerciseOverlay->repositionToParent();
1411 m_exerciseOverlay->show();
1412 m_exerciseOverlay->raise();
1413 });
1414 connect(m_exerciseEngine, &ExerciseEngine::stepChanged,
1415 m_exerciseOverlay, &ExerciseOverlay::onStepChanged);
1416 connect(m_exerciseEngine, &ExerciseEngine::exerciseCompleted,
1417 m_exerciseOverlay, &ExerciseOverlay::onExerciseCompleted);
1418 connect(m_exerciseEngine, &ExerciseEngine::retranslated,
1419 m_exerciseOverlay, &ExerciseOverlay::onRetranslated);
1420 connect(m_exerciseOverlay, &ExerciseOverlay::closeRequested, this, [this] {
1421 if (!m_exerciseOverlay) return;
1422 ExerciseOverlay *overlay = m_exerciseOverlay;
1423 m_exerciseOverlay = nullptr;
1424 m_exerciseEngine->stop();
1425 if (currentTab()) {
1426 currentTab()->setExerciseOverlay(nullptr);
1427 }
1428 if (m_bwd) {
1429 m_bwd->setExerciseOverlay(nullptr);
1430 }
1431 overlay->deleteLater();
1432 });
1433
1434 auto *esc = new QShortcut(QKeySequence(Qt::Key_Escape), m_exerciseOverlay);
1435 connect(esc, &QShortcut::activated, m_exerciseOverlay, &ExerciseOverlay::closeRequested);
1436
1437 currentTab()->setExerciseOverlay(m_exerciseOverlay);
1438 m_exerciseEngine->setScene(currentTab()->scene());
1439 m_exerciseEngine->start();
1440
1441 m_exerciseOverlay->repositionToParent();
1442 m_exerciseOverlay->show();
1443 m_exerciseOverlay->raise();
1444}
1445
1446void MainWindow::startTour(const QString &resourcePath)
1447{
1448 if (resourcePath.isEmpty()) {
1449 return;
1450 }
1451 if (!m_tourEngine->loadFromResource(resourcePath)) {
1452 qWarning() << "TourEngine: failed to load" << resourcePath;
1453 return;
1454 }
1455
1456 delete m_tourOverlay;
1457 m_tourOverlay = new TourOverlay(m_tourEngine, this);
1458 m_tourOverlay->setTargetResolver([this](const QString &id) {
1459 return resolveTourTarget(id);
1460 });
1461
1462 // m_tourEngine outlives any single tour run, so a prior startTour() call's click-handling
1463 // connection (below) must be dropped before reconnecting, or replaying a tour fires
1464 // accumulated handlers once per past run.
1465 disconnect(m_tourEngine, &TourEngine::stepChanged, this, nullptr);
1466
1467 // Must be connected before stepChanged→onStepChanged so click executes before
1468 // resolveTourTarget computes widget rects.
1469 connect(m_tourEngine, &TourEngine::stepChanged, this,
1470 [this](int, int, const TourStep &step) {
1471 for (const QString &id : step.click) clickTarget(id);
1472
1473 TourOverlay *overlay = m_tourOverlay;
1474 if (!overlay) {
1475 return;
1476 }
1477 const bool wantBwd = step.target.startsWith("bwd:");
1478 if (wantBwd && m_bwd) {
1479 if (overlay->parentWidget() != m_bwd) {
1480 overlay->setParentWindow(m_bwd);
1481 }
1482 overlay->show();
1483 overlay->raise();
1484 } else if (!wantBwd && overlay->parentWidget() != this) {
1485 overlay->setParentWindow(this);
1486 overlay->show();
1487 overlay->raise();
1488 }
1489 });
1490 connect(m_tourEngine, &TourEngine::stepChanged,
1491 m_tourOverlay, &TourOverlay::onStepChanged);
1492 connect(m_tourEngine, &TourEngine::tourCompleted,
1493 m_tourOverlay, &TourOverlay::onTourFinished);
1494 connect(m_tourEngine, &TourEngine::tourStopped,
1495 m_tourOverlay, &TourOverlay::onTourFinished);
1496 connect(m_tourEngine, &TourEngine::retranslated,
1497 m_tourOverlay, &TourOverlay::onRetranslated);
1498 connect(m_tourOverlay, &TourOverlay::closeRequested, this, [this] {
1499 if (!m_tourOverlay) return;
1500 TourOverlay *overlay = m_tourOverlay;
1501 m_tourOverlay = nullptr;
1502 m_tourEngine->stop();
1503 overlay->deleteLater();
1504 });
1505
1506 auto *esc = new QShortcut(QKeySequence(Qt::Key_Escape), m_tourOverlay);
1507 connect(esc, &QShortcut::activated, m_tourOverlay, &TourOverlay::closeRequested);
1508
1509 m_tourEngine->start();
1510
1511 m_tourOverlay->show();
1512 m_tourOverlay->raise();
1513}
1514
1515QRect MainWindow::resolveTourTarget(const QString &id) const
1516{
1517 if (!m_tourOverlay || id.isEmpty() || id == "none") {
1518 return {};
1519 }
1520
1521 auto mapWidget = [this](QWidget *w) -> QRect {
1522 if (!w) return {};
1523 const QPoint topLeft = m_tourOverlay->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)));
1524 return QRect(topLeft, w->size());
1525 };
1526
1527 if (id == "toolbar") return mapWidget(m_ui->mainToolBar->widgetForAction(m_ui->actionWaveform));
1528 if (id == "elementPalette") return mapWidget(m_ui->tabElements);
1529 if (id == "gatesTab") {
1530 QTabBar *bar = m_ui->tabElements->tabBar();
1531 const QRect tabRect = bar->tabRect(1);
1532 const QPoint topLeft = m_tourOverlay->mapFromGlobal(bar->mapToGlobal(tabRect.topLeft()));
1533 return QRect(topLeft, tabRect.size());
1534 }
1535 if (id == "ioTab") {
1536 QTabBar *bar = m_ui->tabElements->tabBar();
1537 const QRect tabRect = bar->tabRect(0);
1538 const QPoint topLeft = m_tourOverlay->mapFromGlobal(bar->mapToGlobal(tabRect.topLeft()));
1539 return QRect(topLeft, tabRect.size());
1540 }
1541 if (id == "canvasArea" && currentTab()) return mapWidget(currentTab()->view());
1542 if (id == "elementEditor") return mapWidget(m_ui->elementEditor);
1543 if (id == "searchBar") return mapWidget(m_ui->lineEditSearch);
1544
1545 BewavedDolphin *bwd = m_bwd;
1546 if (id.startsWith("bwd:") && bwd) {
1547 auto mapBwd = [this](QWidget *w) -> QRect {
1548 if (!w || !m_tourOverlay) { // LCOV_EXCL_LINE — !m_tourOverlay is provably unreachable here (the function's own top-level guard above already returned if null, and nothing between there and here can null it out within one synchronous const call); !w would need one of BewavedDolphin's own core widgets (signalTableView()/toolbar action widget/menuBar()) to be null, which none are once it's constructed.
1549 return {}; // LCOV_EXCL_LINE — see above.
1550 }
1551 const QPoint tl = m_tourOverlay->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)));
1552 return QRect(tl, w->size());
1553 };
1554 const QString sub = id.sliced(4);
1555 if (sub == "tableView") return mapBwd(bwd->signalTableView());
1556 if (sub == "toolbar") return mapBwd(bwd->mainToolBar()->widgetForAction(bwd->actionCombinational()));
1557 if (sub == "menuBar") return mapBwd(bwd->menuBar());
1558 return {};
1559 }
1560
1561 return {};
1562}
1563
1564void MainWindow::clickTarget(const QString &id)
1565{
1566 if (id == "ioTab") m_ui->tabElements->setCurrentIndex(0);
1567 else if (id == "gatesTab") m_ui->tabElements->setCurrentIndex(1);
1568 else if (id == "combinational") m_ui->tabElements->setCurrentIndex(2);
1569 else if (id == "memoryTab") m_ui->tabElements->setCurrentIndex(3);
1570 else if (id == "actionPlay") m_ui->actionPlay->trigger();
1571 else if (id == "actionWaveform") m_ui->actionWaveform->trigger();
1572 else if (id == "bwd:actionCombinational" && m_bwd) m_bwd->triggerCombinational();
1573 else if (id == "setupElementEditorDemo") {
1574 if (!currentTab()) {
1575 return;
1576 }
1577 Scene *scene = currentTab()->scene();
1578 scene->clearSelection();
1579 auto *sw = ElementFactory::buildElement(ElementType::InputSwitch);
1580 sw->setPos(0, 0);
1581 // AddItemsCommand registers the element with the scene's Simulation
1582 // (setCircuitUpdateRequired() in redo()) and selects it for us.
1583 scene->receiveCommand(new AddItemsCommand({sw}, scene));
1584 }
1585 else if (id == "setupWaveformDemo") {
1586 if (!currentTab()) {
1587 return;
1588 }
1589 Scene *scene = currentTab()->scene();
1590 scene->clearSelection();
1591 auto *clock1 = ElementFactory::buildElement(ElementType::Clock);
1592 auto *clock2 = ElementFactory::buildElement(ElementType::Clock);
1593 auto *gate = ElementFactory::buildElement(ElementType::And);
1594 auto *led = ElementFactory::buildElement(ElementType::Led);
1595 clock1->setPos(-160, -60);
1596 clock2->setPos(-160, 60);
1597 gate->setPos(0, 0);
1598 led->setPos(160, 0);
1599
1600 auto *conn1 = new Connection();
1601 conn1->setStartPort(clock1->outputPort(0));
1602 conn1->setEndPort(gate->inputPort(0));
1603 auto *conn2 = new Connection();
1604 conn2->setStartPort(clock2->outputPort(0));
1605 conn2->setEndPort(gate->inputPort(1));
1606 auto *conn3 = new Connection();
1607 conn3->setStartPort(gate->outputPort(0));
1608 conn3->setEndPort(led->inputPort(0));
1609
1610 // AddItemsCommand auto-discovers conn1..conn3 via port traversal (loadList())
1611 // and registers all 7 items with the scene's Simulation, fixing the
1612 // frozen-clock bug that raw scene->addItem() caused.
1613 scene->receiveCommand(new AddItemsCommand({clock1, clock2, gate, led}, scene));
1614
1615 conn1->updatePath();
1616 conn2->updatePath();
1617 conn3->updatePath();
1618 }
1619}
Custom QApplication subclass with exception handling and main-window access.
BewavedDolphin waveform editor: digital signal creation, display, and export.
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.
Singleton factory for all circuit element types.
ElementPalette: manages the left-panel element palette and search UI.
Discovery and translation lookup for Exercise/Tour step content.
ExportController: orchestrates exporting the current circuit to its output formats.
Extended QGraphicsView with zoom, pan, and fast-rendering modes.
ICController: integrated-circuit embed / extract / import / removal operations.
Frameless popup widget that shows a preview of an IC's internal circuit.
IC definition registry with file watching and embedded blob storage.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Per-platform resolution of install-relative content directories.
LanguageManager: Qt translation loading and language metadata.
MainWindowUi: hand-written UI class for the MainWindow.
Main application window providing menus, toolbars, and tab management.
Recent-files list management with filesystem watching.
SceneUiBinder: wires the active tab's scene into the shared editor chrome.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
Typed wrappers around QSettings for all application preferences.
Synchronous cycle-based simulation engine with event-driven clock support.
Theme management types and singleton ThemeManager.
UpdateController: drives the application's check-for-updates workflow.
File-format version constants and application version accessor.
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:81
QTableView * signalTableView() const
Returns the waveform table view (for tour target resolution).
QAction * actionCombinational() const
Returns the combinational action (for tour button spotlighting).
QToolBar * mainToolBar() const
Returns the main toolbar (for tour target resolution).
Interface the host application (MainWindow) provides to BewavedDolphin.
Definition DolphinHost.h:24
void openSubcircuitFileRequested(const QString &filePath)
Emitted when the user requests editing a file-backed IC's sub-circuit file.
void editSubcircuitRequested(const QString &blobName, int icElementId)
Emitted when the user requests editing an embedded IC sub-circuit.
void renameAction()
Opens an inline editor to rename the selected element(s).
void extractToFileRequested()
Emitted when the user requests extracting an embedded IC to file.
void changeTriggerAction()
Opens a dialog to change the trigger key for the selected element(s).
void embedSubcircuitRequested()
Emitted when the user requests embedding a file-backed IC.
static GraphicElement * buildElement(const ElementType type)
Constructs and returns a new graphic element of the given type.
Controller for the left-panel element palette, IC list, and search tab.
void addElementRequested(QMimeData *mimeData)
Emitted when the user presses Enter in the search box.
void retranslated()
void stepChanged(int step, int total, const ExerciseStep &data)
void exerciseCompleted()
void onStepChanged(int step, int total, const ExerciseStep &stepData)
void closeRequested()
static QVector< ExerciseTourResourceEntry > discover(const QString &category)
static QString preferredContentDir(const QString &category)
static QString translate(const QString &key, const QString &fallbackEnglish)
Owns the circuit-export workflow: Arduino / SystemVerilog code, PDF / PNG images, and beWavedDolphin ...
void zoomOut()
Decreases the view magnification by one zoom step.
void zoomIn()
Increases the view magnification by one zoom step.
void resetZoom()
Resets the view scale to 1:1.
void setFastMode(const bool fastMode)
Enables or disables fast rendering mode (disables antialiasing).
Owns the IC management workflow: importing file-based ICs into the project directory,...
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 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.
void extractByBlobNameRequested(const QString &blobName)
Emitted when an embedded IC is dropped onto the file-based section.
void embedByFileRequested(const QString &fileName)
Emitted when a file-based IC is dropped onto the embedded section.
Frameless tooltip-like widget showing a rendered preview of an IC's sub-circuit.
static QString resolve(const QString &category)
Owns the application QTranslator instances and provides language metadata.
void translationChanged()
Emitted after a translation has been successfully loaded (or reset to English).
QStringList availableLanguages() const
Returns all available translation language codes, sorted, with "en" first.
virtual QFileInfo icListFile() const =0
File info used to populate the file-based IC palette (parent workspace for inline IC tabs).
void closeEvent(QCloseEvent *event) override
void exportToSystemVerilog(QString fileName)
Generates SystemVerilog code for the current circuit.
QFileInfo currentFile() const override
Returns the QFileInfo of the currently active .panda file.
QWidget * widget() override
~MainWindow() override
void loadPandaFile(const QString &fileName)
Loads a .panda circuit file into the current or a new tab.
void retranslateUi()
Re-applies all translatable strings to the UI after a language change.
void setDolphinFileName(const QString &fileName) override
Associates fileName as the BeWavedDolphin file for the current tab.
ElementPalette * palette() const override
QString shortcutsHelpHtml() const
void createNewTab()
Creates a new empty circuit tab.
QDir currentDir() const override
Returns the directory of the currently active .panda file.
void save(const QString &fileName={}) override
Saves the current circuit to fileName (or prompts if empty).
void exportToArduino(QString fileName)
Generates Arduino sketch code for the current circuit.
QString dolphinFileName() override
Returns the BeWavedDolphin file name associated with the current tab.
WorkSpace * currentTab() const override
Returns the currently visible WorkSpace tab, or nullptr.
void populateLanguageMenu()
Populates the Language submenu with available translations.
void showStatusMessage(const QString &message, int timeout) override
void setFastMode(const bool fastMode)
Enables or disables fast (non-antialiased) rendering for all views.
void requestSave() override
void openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
Opens an embedded IC blob for editing in a new tab.
MainWindow(const QString &fileName={}, QWidget *parent=nullptr)
Constructs the MainWindow and optionally opens fileName on start.
void refreshICButtonsEnabled() override
void loadTranslation(const QString &language)
Loads and installs the Qt translation for language.
void exportToWaveFormTerminal()
Exports the current waveform simulation to standard output (terminal).
bool event(QEvent *event) override
bool hasModifiedFiles()
Returns true if any open workspace has unsaved changes or is a recovered autosave.
void exportToWaveFormFile(const QString &fileName)
Saves the BeWavedDolphin waveform session to fileName.
void show()
Shows the window and initializes child widget state.
DolphinHost * dolphinHost() override
void setICButtonsVisible(bool visible) override
static constexpr int maxFiles
Definition RecentFiles.h:31
void recentFilesUpdated()
Emitted whenever the recent-files list changes (add or file deleted).
void addRecentFile(const QString &filePath)
Prepends filePath to the recent-files list and saves it.
Binds and unbinds the active WorkSpace's scene to the single-instance editor chrome (element editor,...
void unbind()
Tears down the connections established by bind() for the currently bound tab.
void loadFileRequested(const QString &filePath)
The bound scene asked to open a file-based IC by path (forwarded to the tab owner).
void openICRequested(const QString &blobName, int icElementId, const QByteArray &blob)
The bound scene asked to open an embedded IC in a tab (forwarded to the tab owner).
Main circuit editing scene.
Definition Scene.h:56
void flipHorizontally()
Flips selected elements horizontally.
Definition Scene.cpp:893
void alignBottom()
Aligns selected elements' bottom edges to the bottommost selected edge. No-op below 2 elements.
Definition Scene.cpp:1001
void pasteAction()
Pastes items from the internal clipboard into the scene.
Definition Scene.cpp:826
void alignRight()
Aligns selected elements' right edges to the rightmost selected edge. No-op below 2 elements.
Definition Scene.cpp:957
void copyAction()
Copies the selected items to the internal clipboard.
Definition Scene.cpp:814
void rotateLeft()
Rotates selected elements 90 degrees counter-clockwise.
Definition Scene.cpp:869
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
void distributeVertically()
Definition Scene.cpp:1106
void duplicateAction()
Duplicates the selection in place (leaves the system clipboard untouched).
Definition Scene.cpp:832
void selectAll()
Selects all items in the scene.
Definition Scene.cpp:854
void rotateRight()
Rotates selected elements 90 degrees clockwise.
Definition Scene.cpp:863
void alignVerticalCenter()
Definition Scene.cpp:1046
void flipVertically()
Flips selected elements vertically.
Definition Scene.cpp:903
void showWires(bool checked)
Shows or hides connection wires. Delegates to VisibilityManager.
Definition Scene.cpp:738
void deleteAction()
Deletes the currently selected items.
Definition Scene.cpp:838
void alignTop()
Aligns selected elements' top edges to the topmost selected edge. No-op below 2 elements.
Definition Scene.cpp:979
void alignLeft()
Aligns selected elements' left edges to the leftmost selected edge. No-op below 2 elements.
Definition Scene.cpp:935
void distributeHorizontally()
Definition Scene.cpp:1069
void showGates(bool checked)
Shows or hides gate elements. Delegates to VisibilityManager.
Definition Scene.cpp:733
void alignHorizontalCenter()
Definition Scene.cpp:1023
void cutAction()
Cuts the selected items to the internal clipboard.
Definition Scene.cpp:820
static QByteArray splitterState()
Definition Settings.cpp:75
static QByteArray splitterGeometry()
Definition Settings.cpp:65
static void setMainWindowState(const QByteArray &state)
Definition Settings.cpp:60
static void setMinimapVisible(bool visible)
Definition Settings.cpp:295
static void setSplitterGeometry(const QByteArray &geometry)
Definition Settings.cpp:70
static void setMainWindowGeometry(const QByteArray &geometry)
Definition Settings.cpp:50
static bool updateChecksDisabled()
Global opt-out of update checks (for offline/managed installs); default false (enabled).
Definition Settings.cpp:130
static bool minimapVisible()
Definition Settings.cpp:287
static void setIcPreviewDisabled(bool disabled)
Definition Settings.cpp:125
static bool labelsUnderIcons()
Definition Settings.cpp:107
static QString fileName()
Returns the path to the settings file on disk.
Definition Settings.cpp:26
static bool welcomeTourShown()
Definition Settings.cpp:276
static QByteArray mainWindowState()
Definition Settings.cpp:55
static void setSplitterState(const QByteArray &state)
Definition Settings.cpp:80
static void setWelcomeTourShown(bool shown)
Definition Settings.cpp:281
static QByteArray mainWindowGeometry()
Definition Settings.cpp:45
static bool fastMode()
Definition Settings.cpp:97
static void setUpdateChecksDisabled(bool disabled)
Definition Settings.cpp:135
static QString language()
Definition Settings.cpp:142
static void setFastMode(bool enabled)
Definition Settings.cpp:102
static bool icPreviewDisabled()
Definition Settings.cpp:120
static void setLabelsUnderIcons(bool enabled)
Definition Settings.cpp:115
void setUserMuted(bool muted)
Sets whether the user has explicitly muted audio; persists across stop/start cycles.
void restart()
void start()
Starts the 1 ms simulation timer.
static Theme theme()
Returns the currently active theme.
static ThemeManager & instance()
Returns the singleton ThemeManager instance.
void themeChanged()
Emitted whenever the active theme changes.
static void setTheme(const Theme theme)
Switches the application to theme and emits themeChanged().
void tourStopped()
void retranslated()
void stepChanged(int step, int total, const TourStep &data)
void tourCompleted()
void onTourFinished()
void closeRequested()
void onRetranslated()
void onStepChanged(int step, int total, const TourStep &stepData)
void setParentWindow(QWidget *newParent)
Re-parents the overlay to newParent and reinstalls the resize event filter.
void removeEmbeddedIC(const QString &blobName)
Removes all embedded IC instances with the given blobName from the scene.
void removeICFile(const QString &icFileName)
Removes the IC file icFileName from disk after user confirmation.
Owns the update-check lifecycle: querying for a newer release and presenting the resulting notificati...
A widget containing a complete circuit editing environment.
Definition Workspace.h:33
bool isInlineIC() const
Returns true if this workspace is editing an embedded IC blob (not a file).
Definition Workspace.h:100
Scene * scene()
Returns the Scene embedded in this workspace.
void setMinimapVisible(bool visible)
QString dolphinFileName() const
Returns the path of the associated BeWavedDolphin waveform file.
void setExerciseOverlay(ExerciseOverlay *overlay)
Simulation * simulation()
Returns the embedded Simulation.
GraphicsView * view()
Returns the GraphicsView embedded in this workspace.
QFileInfo fileInfo() const
Returns the file info for the currently open circuit file.
Owns the document/tab model: the current tab, tab creation/closing/switching, and the file open/save/...
void recentFileAdded(const QString &filePath)
Emitted when a file-backed tab is (re)named, to feed the recent-files list.
bool closeTab(int tabIndex)
Closes the tab at tabIndex (prompting to save if needed). Returns false if cancelled.
void openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
void onCurrentIndexChanged(int newIndex)
Reacts to QTabWidget::currentChanged: updates the current tab and emits currentTabChanged.
void currentTabChanged(WorkSpace *tab)
Emitted when the active tab changes (or becomes null); the shell rebinds the chrome.
QFileInfo icListFile() const
void loadPandaFile(const QString &fileName)
const QVersionNumber current
Definition Versions.h:70
QStringList click
Widget/action IDs to activate on step enter.
QString context
"" or "circuit" → overlay on WorkSpace; "bwd" → overlay on BeWavedDolphin.
QStringList click
Widget/action IDs to activate on step enter.
Definition TourStep.h:19
QString target
Definition TourStep.h:18