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"
49#include "App/Element/IC.h"
53#include "App/IO/RecentFiles.h"
54#include "App/Scene/Commands.h"
57#include "App/Scene/Workspace.h"
59#include "App/Tour/TourEngine.h"
63#include "App/UI/ICController.h"
65#include "App/UI/MainWindowUI.h"
69#include "App/Versions.h"
71
72#ifdef Q_OS_MAC
73void ensureSvgUsage() {
74 QSvgRenderer dummy; // for macdeployqt to add libqsvg.dylib
75}
76#endif
77
78#ifdef Q_OS_WASM
79const char *MainWindow::onBeforeUnload(int /*eventType*/, const void * /*reserved*/, void *userData)
80{
81 static_cast<MainWindow *>(userData)->updateSettings();
82 return nullptr;
83}
84#endif
85
86MainWindow::MainWindow(const QString &fileName, QWidget *parent)
87 : QMainWindow(parent)
88 , m_ui(std::make_unique<MainWindowUi>())
89{
90 qCDebug(zero) << "wiRedPanda Version = " APP_VERSION " OR " << AppVersion::current;
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 QLocale systemLocale = QLocale::system();
177 const QString systemLang = systemLocale.name();
178 const QString baseLang = systemLang.split('_').first();
179 qCDebug(zero) << "Auto-detected system locale:" << systemLang;
180
181 const auto available = m_languageManager->availableLanguages();
182 if (available.contains(systemLang)) {
183 language = systemLang;
184 } else if (available.contains(baseLang)) {
185 language = baseLang;
186 } else {
187 qCDebug(zero) << "No translation for" << systemLang << "or" << baseLang << ", falling back to English";
188 language = "en";
189 }
190 qCDebug(zero) << "Selected language:" << language;
191 }
192
193 m_languageManager->loadTranslation(language);
195}
196
197void MainWindow::setupGeometry()
198{
199 qCDebug(zero) << "Restoring geometry and setting zoom controls.";
200 restoreGeometry(Settings::mainWindowGeometry());
201 restoreState(Settings::mainWindowState());
202 m_ui->splitter->restoreGeometry(Settings::splitterGeometry());
203 m_ui->splitter->restoreState(Settings::splitterState());
204}
205
206void MainWindow::setupTheme()
207{
208 qCDebug(zero) << "Preparing theme and UI modes.";
209 auto *themeGroup = new QActionGroup(this);
210 for (auto *action : m_ui->menuTheme->actions()) {
211 themeGroup->addAction(action);
212 }
213 themeGroup->setExclusive(true);
214
215 connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, &MainWindow::updateTheme);
216 updateTheme();
218
219 // Restore toolbar label style from previous session.
220 m_ui->actionLabelsUnderIcons->setChecked(Settings::labelsUnderIcons());
221 m_ui->mainToolBar->setToolButtonStyle(Settings::labelsUnderIcons() ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly);
222
223 // Restore IC hover-preview visibility from previous session.
224 m_ui->actionICPreview->setChecked(!Settings::icPreviewDisabled());
225 m_ui->actionCheckForUpdates->setChecked(!Settings::updateChecksDisabled());
226}
227
228void MainWindow::setupRecentFiles()
229{
230 m_recentFiles = new RecentFiles(this);
231 connect(m_workspaceManager, &WorkspaceManager::recentFileAdded, m_recentFiles, &RecentFiles::addRecentFile);
232 connect(m_recentFiles, &RecentFiles::recentFilesUpdated, this, &MainWindow::updateRecentFileActions);
233 createRecentFileActions();
234}
235
236void MainWindow::setupExamplesMenu()
237{
238 const QString examplesPath = InstallRelativePaths::resolve(QStringLiteral("Examples"));
239
240 if (!examplesPath.isEmpty()) {
241 const auto entryList = QDir(examplesPath).entryList({"*.panda"}, QDir::Files);
242
243 for (const auto &entry : entryList) {
244 // Show a prettified title ("display-4bits-counter.panda" -> "Display 4bits
245 // Counter") but keep the real path in setData() so the lookup never depends on
246 // the (translatable, prettified) label — same pattern as the Recent Files menu.
247 QString title = QFileInfo(entry).completeBaseName();
248 title.replace(QLatin1Char('-'), QLatin1Char(' '));
249 title.replace(QLatin1Char('_'), QLatin1Char(' '));
250 QStringList words = title.split(QLatin1Char(' '), Qt::SkipEmptyParts);
251 for (QString &word : words) {
252 word[0] = word[0].toUpper();
253 }
254
255 auto *action = new QAction(words.join(QLatin1Char(' ')), this);
256 action->setData(examplesPath + "/" + entry);
257
258 connect(action, &QAction::triggered, this, [this] {
259 if (auto *senderAction = qobject_cast<QAction *>(sender())) {
260 loadPandaFile(senderAction->data().toString());
261 }
262 });
263
264 m_ui->menuExamples->addAction(action);
265 }
266 }
267
268 if (m_ui->menuExamples->isEmpty()) {
269 m_ui->menuExamples->menuAction()->setVisible(false);
270 }
271}
272
273void MainWindow::populateContentMenu(QMenu *menu, const QString &categoryKey,
274 const QString &openFolderText,
275 const QString &openFolderFailureText,
276 const QStringList &completed,
277 const std::function<void(const QString &)> &onSelect)
278{
279 menu->clear(); // deletes the QActions it owns (and their connections) — safe to rebuild every open
280
281 auto *openFolderAction = new QAction(openFolderText, menu);
282 connect(openFolderAction, &QAction::triggered, this, [this, categoryKey, openFolderFailureText] {
283 const QString dir = ExerciseTourResources::preferredContentDir(categoryKey);
284 if (dir.isEmpty()) {
285 QMessageBox::warning(this, tr("Error"), openFolderFailureText);
286 return;
287 }
288 QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
289 });
290 menu->addAction(openFolderAction);
291
292 const QIcon checkIcon = style()->standardIcon(QStyle::SP_DialogApplyButton);
293 const QIcon circleIcon = style()->standardIcon(QStyle::SP_ArrowRight);
294
295 const auto entries = ExerciseTourResources::discover(categoryKey);
296 if (!entries.isEmpty()) {
297 menu->addSeparator(); // only meaningful as a divider when there's something below it to divide from
298 }
299 for (const ExerciseTourResourceEntry &entry : entries) {
300 QString title = ExerciseTourResources::translate(entry.id + QStringLiteral(".title"), entry.title);
301 title.replace(QLatin1Char('&'), QStringLiteral("&&")); // literal '&' would otherwise be swallowed as a mnemonic marker
302 auto *action = new QAction(title, menu);
303 action->setIcon(completed.contains(entry.id) ? checkIcon : circleIcon);
304 action->setStatusTip(ExerciseTourResources::translate(entry.id + QStringLiteral(".description"), entry.description));
305 connect(action, &QAction::triggered, this, [onSelect, path = entry.path] { onSelect(path); });
306 menu->addAction(action);
307 }
308}
309
310void MainWindow::setupExercisesMenu()
311{
312 connect(m_ui->menuExercises, &QMenu::aboutToShow, this, [this] {
313 populateContentMenu(m_ui->menuExercises, "Exercises",
314 tr("Open My Exercises Folder"),
315 tr("Could not create or access a folder for custom exercises."),
316 Settings::completedExercises(),
317 [this](const QString &path) { startExercise(path); });
318 });
319}
320
321void MainWindow::setupToursMenu()
322{
323 connect(m_ui->menuTours, &QMenu::aboutToShow, this, [this] {
324 populateContentMenu(m_ui->menuTours, "Tours",
325 tr("Open My Tours Folder"),
326 tr("Could not create or access a folder for custom tours."),
327 Settings::completedTours(),
328 [this](const QString &path) { startTour(path); });
329 });
330}
331
332void MainWindow::setupShortcuts()
333{
334 // The scene-property shortcuts ( [ ] { } < > ) are owned by SceneUiBinder, which
335 // re-targets them to the active tab's scene on each switch.
336 auto *searchShortcut = new QShortcut(QKeySequence("Ctrl+F"), this);
337 connect(searchShortcut, &QShortcut::activated, m_ui->lineEditSearch, qOverload<>(&QWidget::setFocus));
338}
339
340void MainWindow::setupConnections()
341{
342 connect(m_ui->tab, &QTabWidget::currentChanged, m_workspaceManager, &WorkspaceManager::onCurrentIndexChanged);
343 connect(m_ui->tab, &QTabWidget::tabCloseRequested, m_workspaceManager, &WorkspaceManager::closeTab);
344
345 connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::on_actionAbout_triggered);
346 connect(m_ui->actionAboutQt, &QAction::triggered, this, &MainWindow::on_actionAboutQt_triggered);
347 connect(m_ui->actionAboutThisVersion, &QAction::triggered, this, &MainWindow::aboutThisVersion);
348 connect(m_ui->actionReportTranslationError,&QAction::triggered, this, &MainWindow::on_actionReportTranslationError_triggered);
349 connect(m_ui->actionChangeTrigger, &QAction::triggered, m_ui->elementEditor, &ElementEditor::changeTriggerAction);
350 connect(m_ui->actionDarkTheme, &QAction::triggered, this, &MainWindow::on_actionDarkTheme_triggered);
351 connect(m_ui->actionSystemTheme, &QAction::triggered, this, &MainWindow::on_actionSystemTheme_triggered);
352 connect(m_ui->actionExit, &QAction::triggered, this, &MainWindow::on_actionExit_triggered);
353 connect(m_ui->actionExportToArduino, &QAction::triggered, m_exportController, &ExportController::exportArduinoDialog);
354 connect(m_ui->actionExportToSystemVerilog, &QAction::triggered, m_exportController, &ExportController::exportSystemVerilogDialog);
355 connect(m_ui->actionExportToImage, &QAction::triggered, m_exportController, &ExportController::exportImageDialog);
356 connect(m_ui->actionExportToPdf, &QAction::triggered, m_exportController, &ExportController::exportPdfDialog);
357 connect(m_ui->actionFastMode, &QAction::triggered, this, &MainWindow::on_actionFastMode_triggered);
358 connect(m_ui->actionFlipHorizontally, &QAction::triggered, this, &MainWindow::on_actionFlipHorizontally_triggered);
359 connect(m_ui->actionFlipVertically, &QAction::triggered, this, &MainWindow::on_actionFlipVertically_triggered);
360 connect(m_ui->actionAlignLeft, &QAction::triggered, this, &MainWindow::on_actionAlignLeft_triggered);
361 connect(m_ui->actionAlignRight, &QAction::triggered, this, &MainWindow::on_actionAlignRight_triggered);
362 connect(m_ui->actionAlignTop, &QAction::triggered, this, &MainWindow::on_actionAlignTop_triggered);
363 connect(m_ui->actionAlignBottom, &QAction::triggered, this, &MainWindow::on_actionAlignBottom_triggered);
364 connect(m_ui->actionAlignHorizontalCenter, &QAction::triggered, this, &MainWindow::on_actionAlignHorizontalCenter_triggered);
365 connect(m_ui->actionAlignVerticalCenter, &QAction::triggered, this, &MainWindow::on_actionAlignVerticalCenter_triggered);
366 connect(m_ui->actionDistributeHorizontally, &QAction::triggered, this, &MainWindow::on_actionDistributeHorizontally_triggered);
367 connect(m_ui->actionDistributeVertically, &QAction::triggered, this, &MainWindow::on_actionDistributeVertically_triggered);
368 connect(m_ui->actionFullscreen, &QAction::triggered, this, &MainWindow::on_actionFullscreen_triggered);
369 connect(m_ui->actionGates, &QAction::triggered, this, &MainWindow::on_actionGates_triggered);
370 connect(m_ui->actionLabelsUnderIcons, &QAction::triggered, this, &MainWindow::on_actionLabelsUnderIcons_triggered);
371 connect(m_ui->actionICPreview, &QAction::triggered, this, &MainWindow::on_actionICPreview_triggered);
372 connect(m_ui->actionCheckForUpdates, &QAction::triggered, this, &MainWindow::on_actionCheckForUpdates_triggered);
373 connect(m_ui->actionShowMinimap, &QAction::triggered, this, &MainWindow::on_actionShowMinimap_triggered);
374 connect(m_ui->actionLightTheme, &QAction::triggered, this, &MainWindow::on_actionLightTheme_triggered);
375 connect(m_ui->actionMute, &QAction::triggered, this, &MainWindow::on_actionMute_triggered);
376 connect(m_ui->actionNew, &QAction::triggered, m_workspaceManager, &WorkspaceManager::newTab);
377 connect(m_ui->actionOpen, &QAction::triggered, m_workspaceManager, &WorkspaceManager::openFile);
378 connect(m_ui->actionPlay, &QAction::toggled, this, &MainWindow::on_actionPlay_toggled);
379 connect(m_ui->actionReloadFile, &QAction::triggered, m_workspaceManager, &WorkspaceManager::reloadFile);
380 connect(m_ui->actionRename, &QAction::triggered, m_ui->elementEditor, &ElementEditor::renameAction);
381
382 // ElementEditor IC sub-circuit actions
383 connect(m_ui->elementEditor, &ElementEditor::editSubcircuitRequested, this, [this](const QString &blobName, int icElementId) {
384 if (currentTab()) {
385 openICInTab(blobName, icElementId, currentTab()->scene()->icRegistry()->blob(blobName));
386 }
387 });
388 connect(m_ui->elementEditor, &ElementEditor::openSubcircuitFileRequested, this, &MainWindow::loadPandaFile);
389 connect(m_ui->elementEditor, &ElementEditor::embedSubcircuitRequested, m_icController, &ICController::embedSelectedIC);
390 connect(m_ui->elementEditor, &ElementEditor::extractToFileRequested, m_icController, &ICController::extractSelectedIC);
391 connect(m_ui->actionResetZoom, &QAction::triggered, this, &MainWindow::on_actionResetZoom_triggered);
392 connect(m_ui->actionZoomToFit, &QAction::triggered, this, &MainWindow::on_actionZoomToFit_triggered);
393 connect(m_ui->actionRestart, &QAction::triggered, this, &MainWindow::on_actionRestart_triggered);
394 connect(m_ui->actionRotateLeft, &QAction::triggered, this, &MainWindow::on_actionRotateLeft_triggered);
395 connect(m_ui->actionRotateRight, &QAction::triggered, this, &MainWindow::on_actionRotateRight_triggered);
396 connect(m_ui->actionSave, &QAction::triggered, m_workspaceManager, &WorkspaceManager::saveFile);
397 connect(m_ui->actionSaveAs, &QAction::triggered, m_workspaceManager, &WorkspaceManager::saveFileAs);
398 connect(m_ui->actionSelectAll, &QAction::triggered, this, &MainWindow::on_actionSelectAll_triggered);
399 connect(m_ui->actionShortcutsAndTips, &QAction::triggered, this, &MainWindow::on_actionShortcuts_and_Tips_triggered);
400 connect(m_ui->actionWaveform, &QAction::triggered, this, &MainWindow::on_actionWaveform_triggered);
401 connect(m_ui->actionWires, &QAction::triggered, this, &MainWindow::on_actionWires_triggered);
402 connect(m_ui->actionZoomIn, &QAction::triggered, this, &MainWindow::on_actionZoomIn_triggered);
403 connect(m_ui->actionZoomOut, &QAction::triggered, this, &MainWindow::on_actionZoomOut_triggered);
404 connect(m_palette, &ElementPalette::addElementRequested, this, [this](QMimeData *mimeData) {
405 auto *tab = currentTab();
406 if (!tab) {
407 delete mimeData; // no scene to take ownership; don't leak the payload
408 return;
409 }
410 // Land the element at the centre of what the user is currently viewing, not the
411 // scene origin, so it's visible however the canvas is scrolled or zoomed.
412 auto *view = tab->view();
413 const QPointF center = view->mapToScene(view->viewport()->rect().center());
414 tab->scene()->addItem(mimeData, center);
415 });
416 connect(m_ui->pushButtonAddIC, &QPushButton::clicked, m_icController, &ICController::addICFromFile);
417 connect(m_ui->pushButtonRemoveIC, &QPushButton::clicked, m_icController, &ICController::showRemoveICHint);
418 connect(m_ui->pushButtonRemoveIC, &TrashButton::removeICFile,m_icController, &ICController::removeICFile);
419 connect(m_ui->pushButtonMakeSelfContained, &QPushButton::clicked, m_icController, &ICController::makeSelfContained);
420 connect(m_ui->actionMakeSelfContained, &QAction::triggered, m_icController, &ICController::makeSelfContained);
421
422 // ICDropZone cross-section drag-and-drop
423 connect(m_ui->dropZoneFileBased, &ICDropZone::extractByBlobNameRequested, m_icController, &ICController::extractICByBlobName);
424 connect(m_ui->dropZoneEmbedded, &ICDropZone::embedByFileRequested, m_icController, &ICController::embedICByFile);
425
426 // Embedded IC section buttons
427 connect(m_ui->pushButtonAddEmbeddedIC, &QPushButton::clicked, m_icController, &ICController::addEmbeddedICFromFile);
428 connect(m_ui->pushButtonRemoveEmbeddedIC, &QPushButton::clicked, m_icController, &ICController::showRemoveICHint);
429 connect(m_ui->pushButtonRemoveEmbeddedIC, &TrashButton::removeEmbeddedIC, m_icController, &ICController::removeEmbeddedIC);
430
431 // These edit actions always delegate to the current tab's scene, so they
432 // never need to be rewired on tab switch.
433 connectSceneAction(m_ui->actionClearSelection, &Scene::clearSelection);
434 connectSceneAction(m_ui->actionCopy, &Scene::copyAction);
435 connectSceneAction(m_ui->actionCut, &Scene::cutAction);
436 connectSceneAction(m_ui->actionDelete, &Scene::deleteAction);
437 connectSceneAction(m_ui->actionPaste, &Scene::pasteAction);
438 connectSceneAction(m_ui->actionDuplicate, &Scene::duplicateAction);
439}
440
441void MainWindow::connectSceneAction(QAction *action, void (Scene::*method)())
442{
443 connect(action, &QAction::triggered, this, [this, method] {
444 if (currentTab()) {
445 (currentTab()->scene()->*method)();
446 }
447 });
448}
449
451{
452 // Tear down the active tab's chrome wiring before child objects are destroyed.
453 m_binder->unbind();
454}
455
457{
458 m_workspaceManager->createNewTab();
459}
460
461void MainWindow::setFastMode(const bool fastMode)
462{
463 m_ui->actionFastMode->setChecked(fastMode);
464
465 if (currentTab()) {
466 currentTab()->view()->setFastMode(fastMode);
467 }
468}
469
470void MainWindow::on_actionExit_triggered()
471{
472 Application::guardedSlot(this, [this] {
473 close();
474 });
475}
476
477void MainWindow::save(const QString &fileName)
478{
479 m_workspaceManager->save(fileName);
480}
481
483{
484 QMainWindow::show();
485
486 qCDebug(zero) << "Checking for autosave file recovery.";
487 m_workspaceManager->loadAutosaveFiles();
488
489 auto *updateController = new UpdateController(this);
490 updateController->checkForUpdates();
491
492 // First-ever launch: auto-start the built-in UI walkthrough. Gated on interactiveMode so
493 // it doesn't fire in the CLI/MCP-automation paths that also construct a MainWindow.
496 startTour(QStringLiteral(":/Tours/ui-overview.json"));
497 }
498}
499
500void MainWindow::aboutThisVersion()
501{
502 QMessageBox msgBox;
503 msgBox.setParent(this);
504 msgBox.setStandardButtons(QMessageBox::Ok);
505 msgBox.setIcon(QMessageBox::Icon::Information);
506 msgBox.setWindowTitle("wiRedPanda " APP_VERSION);
507 msgBox.setText(
508 tr("wiRedPanda %1\n\n"
509 "This version includes automatic migration of older project files.\n"
510 "When you open a project file older than the current version, it will be automatically "
511 "upgraded to the current format and a versioned backup will be created.\n\n"
512 "To open projects containing ICs (or boxes), appearances, and/or beWavedDolphin simulations, "
513 "their files must be in the same directory as the main project file.\n"
514 "wiRedPanda %1 will automatically list all other .panda files located "
515 "in the same directory as the current project as ICs in the editor tab.\n"
516 "You have to save new projects before accessing ICs and appearances, or running "
517 "beWavedDolphin simulations.").arg(APP_VERSION));
518 msgBox.setWindowModality(Qt::WindowModal);
519 msgBox.setDefaultButton(QMessageBox::Ok);
520
521 msgBox.exec();
522}
523
524void MainWindow::on_actionWires_triggered(const bool checked)
525{
526 Application::guardedSlot(this, [this, checked] {
527 sentryBreadcrumb("ui", QStringLiteral("Wires: %1").arg(checked));
528 if (currentTab()) {
529 currentTab()->scene()->showWires(checked);
530 }
531 });
532}
533
534void MainWindow::on_actionRotateRight_triggered()
535{
536 Application::guardedSlot(this, [this] {
537 if (currentTab()) {
539 }
540 });
541}
542
543void MainWindow::on_actionRotateLeft_triggered()
544{
545 Application::guardedSlot(this, [this] {
546 if (currentTab()) {
548 }
549 });
550}
551
552void MainWindow::loadPandaFile(const QString &fileName)
553{
554 m_workspaceManager->loadPandaFile(fileName);
555}
556
557void MainWindow::openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
558{
559 m_workspaceManager->openICInTab(blobName, icElementId, blob);
560}
561
562void MainWindow::on_actionAbout_triggered()
563{
564 Application::guardedSlot(this, [this] {
565 QMessageBox::about(
566 this,
567 "wiRedPanda",
568 tr("<p>wiRedPanda is software developed by students of the Federal University of São Paulo"
569 " to help students learn about logic circuits.</p>"
570 "<p>Software version: %1</p>"
571 "<p><strong>Creators:</strong></p>"
572 "<ul>"
573 "<li> Davi Morales </li>"
574 "<li> Lucas Lellis </li>"
575 "<li> Rodrigo Torres </li>"
576 "<li> Prof. Fábio Cappabianco, Ph.D. </li>"
577 "</ul>"
578 "<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>"
579 "<p> Please file a report at our GitHub page if you find a bug or want to request a new feature.</p>"
580 "<p><a href=\"https://gibis-unifesp.github.io/wiRedPanda/\">Visit our website!</a></p>")
581 .arg(QApplication::applicationVersion()));
582 });
583}
584
586{
587 // Keyed by label so rows sort alphabetically and any duplicate collapses.
588 QMap<QString, QString> byLabel;
589
590 // Undo/Redo belong to the active Scene's stack, not this window, so findChildren()
591 // can't see them; add them explicitly with the platform-standard chords.
592 byLabel.insert(tr("Redo"), QKeySequence(QKeySequence::Redo).toString(QKeySequence::NativeText));
593 byLabel.insert(tr("Undo"), QKeySequence(QKeySequence::Undo).toString(QKeySequence::NativeText));
594
595 const auto actions = findChildren<QAction *>();
596 for (const auto *action : actions) {
597 const QKeySequence seq = action->shortcut();
598 if (seq.isEmpty()) {
599 continue;
600 }
601 QString label = action->text();
602 label.remove(QLatin1Char('&')); // strip menu mnemonics
603 if (label.endsWith(QLatin1String("..."))) {
604 label.chop(3);
605 }
606 label = label.trimmed();
607 if (!label.isEmpty()) {
608 byLabel.insert(label, seq.toString(QKeySequence::NativeText));
609 }
610 }
611
612 QString rows;
613 for (auto it = byLabel.cbegin(); it != byLabel.cend(); ++it) {
614 rows += QStringLiteral("<tr><td><b>%1</b>&nbsp;&nbsp;&nbsp;</td><td>%2</td></tr>")
615 .arg(it.value().toHtmlEscaped(), it.key().toHtmlEscaped());
616 }
617
618 return tr("<h1>Keyboard Shortcuts</h1>"
619 "<table>%1</table>"
620 "<h1>Element Property Navigation</h1>"
621 "<ul style=\"list-style:none;\">"
622 "<li> [ / ] : Previous / next primary property </li>"
623 "<li> { / } : Previous / next secondary property </li>"
624 "<li> &lt; / &gt; : Morph to previous / next element </li>"
625 "</ul>"
626 "<h1>General Tips</h1>"
627 "<ul style=\"list-style:none;\">"
628 "<li> Double-click a wire to create a node </li>"
629 "<li> Drag an element from the left panel onto the canvas to add it </li>"
630 "<li> Nudge the selection with the arrow keys (hold Shift for larger steps) </li>"
631 "<li> Drop a .panda file onto the canvas to open it </li>"
632 "</ul>")
633 .arg(rows);
634}
635
636void MainWindow::on_actionShortcuts_and_Tips_triggered()
637{
638 Application::guardedSlot(this, [this] {
639 QMessageBox::information(this, tr("Shortcuts and Tips"), shortcutsHelpHtml());
640 });
641}
642
643void MainWindow::on_actionAboutQt_triggered()
644{
645 Application::guardedSlot(this, [this] {
646 QMessageBox::aboutQt(this);
647 });
648}
649
650void MainWindow::on_actionReportTranslationError_triggered()
651{
652 Application::guardedSlot(this, [] {
653 QDesktopServices::openUrl(QUrl("https://hosted.weblate.org/projects/wiredpanda/wiredpanda"));
654 });
655}
656
658{
659 bool closeWindow = false;
660
661 // If nothing is modified, ask once before exiting so the user can't
662 // accidentally quit with a keyboard shortcut. If there are unsaved changes,
663 // delegate to closeFiles() which prompts per-tab.
664 if (!m_workspaceManager->hasModifiedFiles()) {
665 auto reply =
666 QMessageBox::question(
667 this,
668 tr("Exit") + " " + QApplication::applicationName(),
669 tr("Are you sure?"),
670 QMessageBox::Cancel | QMessageBox::Yes,
671 QMessageBox::Yes);
672
673 if (reply == QMessageBox::Yes) {
674 closeWindow = true;
675 }
676 } else if (m_workspaceManager->closeFiles()) {
677 closeWindow = true;
678 }
679
680 if (closeWindow) {
681 // Persist window/splitter layout so the next session opens the same way.
682 updateSettings();
683 event->accept();
684 } else {
685 event->ignore();
686 }
687}
688
689void MainWindow::updateSettings()
690{
691 Settings::setMainWindowGeometry(saveGeometry());
692 Settings::setMainWindowState(saveState());
693 Settings::setSplitterGeometry(m_ui->splitter->saveGeometry());
694 Settings::setSplitterState(m_ui->splitter->saveState());
695}
696
698{
699 m_ui->pushButtonAddIC->setVisible(visible);
700 m_ui->pushButtonRemoveIC->setVisible(visible);
701 m_ui->pushButtonMakeSelfContained->setVisible(visible);
702}
703
705{
706 // Add IC needs a real project directory to copy the chosen .panda into;
707 // gating click-ability on a saved file avoids the "Save file first."
708 // throw → modal-error UX dead end.
709 const bool hasFile = currentTab() && currentTab()->fileInfo().isReadable();
710 m_ui->pushButtonAddIC->setEnabled(hasFile);
711}
712
713QFileInfo MainWindow::currentFile() const
714{
715 return m_workspaceManager->currentFile();
716}
717
719{
720 return m_workspaceManager->hasModifiedFiles();
721}
722
724{
725 return m_workspaceManager->currentDir();
726}
727
729{
730 return this;
731}
732
734{
735 return this;
736}
737
739{
740 return m_palette;
741}
742
744{
745 m_workspaceManager->saveFile();
746}
747
748void MainWindow::showStatusMessage(const QString &message, int timeout)
749{
750 m_ui->statusBar->showMessage(message, timeout);
751}
752
753QFileInfo MainWindow::icListFile() const
754{
755 return m_workspaceManager->icListFile();
756}
757
758void MainWindow::on_actionSelectAll_triggered()
759{
760 Application::guardedSlot(this, [this] {
761 sentryBreadcrumb("ui", QStringLiteral("Select all"));
762 if (!currentTab()) {
763 return;
764 }
765
766 currentTab()->scene()->selectAll();
767 });
768}
769
771{
772 return m_workspaceManager->currentTab();
773}
774
775void MainWindow::onCurrentTabChanged(WorkSpace *newTab)
776{
777 // Reaction to WorkspaceManager::currentTabChanged: rebind the shared chrome to the
778 // new scene and refresh the tab-navigation view state (file-based IC list, buttons).
779 // currentTab() already reflects newTab here (WorkspaceManager updates its current-tab
780 // field before emitting this signal), so the tab actually being left is tracked
781 // separately in m_previousTab rather than read via currentTab().
782 WorkSpace *prevTab = m_previousTab;
783 m_previousTab = newTab;
784
785 m_binder->unbind(); // tear down the previously bound tab's chrome wiring
786 // Hide the editor panel during the transition; SceneUiBinder::bind restores it
787 // once the new scene's selection is known.
788 m_ui->elementEditor->hide();
789
790 // Detach exercise overlay from the leaving tab
791 if (prevTab) {
792 prevTab->setExerciseOverlay(nullptr);
793 }
794 if (m_exerciseOverlay && m_exerciseEngine && m_exerciseEngine->isActive()) {
795 m_exerciseOverlay->hide();
796 m_exerciseOverlay->setParent(nullptr);
797 }
798
799 if (!newTab) {
800 // All tabs were closed; reset state.
801 m_palette->updateICList(QFileInfo());
802 m_palette->updateEmbeddedICList(nullptr);
803 updateWindowTitle();
804 return;
805 }
806
807 m_binder->bind(newTab);
808 m_palette->updateICList(icListFile());
809
810 // Apply the minimap visibility preference to the newly activated tab. Position/size are
811 // per-tab (each WorkSpace restores its own from Settings::minimapGeometry() on first
812 // layout), so there's nothing to push here for those.
814
815 // Hide management buttons for inline IC tabs (they use currentFile/currentDir which are empty)
818
819 // Re-attach exercise overlay to the arriving tab
820 if (m_exerciseEngine && m_exerciseEngine->isActive() && m_exerciseOverlay) {
821 m_exerciseEngine->setScene(newTab->scene());
822 m_exerciseOverlay->setParent(newTab);
823 newTab->setExerciseOverlay(m_exerciseOverlay);
824 m_exerciseOverlay->repositionToParent();
825 m_exerciseOverlay->show();
826 m_exerciseOverlay->raise();
827 }
828
829 updateWindowTitle();
830}
831
832void MainWindow::updateWindowTitle()
833{
834 auto *tab = currentTab();
835 if (!tab) {
836 setWindowTitle(QStringLiteral("wiRedPanda " APP_VERSION));
837 setWindowModified(false);
838 return;
839 }
840
841 // "<name>[*] — wiRedPanda <version>": Qt swaps "[*]" for "*" when the window is marked
842 // modified, and drops it otherwise (a native dot on macOS).
843 setWindowTitle(tr("%1[*] — wiRedPanda %2")
844 .arg(m_workspaceManager->currentTabName(), QStringLiteral(APP_VERSION)));
845 setWindowModified(!tab->scene()->undoStack()->isClean());
846}
847
848void MainWindow::on_actionGates_triggered(const bool checked)
849{
850 Application::guardedSlot(this, [this, checked] {
851 sentryBreadcrumb("ui", QStringLiteral("Gates: %1").arg(checked));
852 if (!currentTab()) {
853 return;
854 }
855
856 // Wire visibility depends on gates being visible: if gates are hidden, wires
857 // make no sense and should be hidden too. Re-enable the wire toggle only when
858 // gates are shown so the user can't end up with floating wires.
859 m_ui->actionWires->setEnabled(checked);
860 currentTab()->scene()->showWires(checked ? m_ui->actionWires->isChecked() : checked);
861 currentTab()->scene()->showGates(checked);
862 });
863}
864
865void MainWindow::exportToArduino(QString fileName)
866{
867 m_exportController->exportToArduino(std::move(fileName));
868}
869
871{
872 m_exportController->exportToSystemVerilog(std::move(fileName));
873}
874
875void MainWindow::exportToWaveFormFile(const QString &fileName)
876{
877 m_exportController->exportToWaveFormFile(fileName);
878}
879
881{
882 m_exportController->exportToWaveFormTerminal();
883}
884
885void MainWindow::on_actionZoomIn_triggered() const
886{
887 Application::guardedSlot(this, [this] {
888 if (!currentTab()) {
889 return;
890 }
891
892 currentTab()->view()->zoomIn();
893 });
894}
895
896void MainWindow::on_actionZoomOut_triggered() const
897{
898 Application::guardedSlot(this, [this] {
899 if (!currentTab()) {
900 return;
901 }
902
903 currentTab()->view()->zoomOut();
904 });
905}
906
907void MainWindow::on_actionResetZoom_triggered() const
908{
909 Application::guardedSlot(this, [this] {
910 if (!currentTab()) {
911 return;
912 }
913
914 currentTab()->view()->resetZoom();
915 });
916}
917
918void MainWindow::on_actionZoomToFit_triggered() const
919{
920 Application::guardedSlot(this, [this] {
921 if (!currentTab()) {
922 return;
923 }
924
925 currentTab()->view()->zoomToFit();
926 });
927}
928
929void MainWindow::updateRecentFileActions()
930{
931 const auto files = m_recentFiles->recentFiles();
932 const int numRecentFiles = static_cast<int>(qMin(files.size(), RecentFiles::maxFiles));
933
934 if (numRecentFiles > 0) {
935 m_ui->menuRecentFiles->setEnabled(true);
936 }
937
938 auto actions = m_ui->menuRecentFiles->actions();
939
940 // The menu has exactly RecentFiles::maxFiles pre-allocated actions; update
941 // visible ones in order, hide the rest. Prefix "&1", "&2" … adds a mnemonic
942 // so the entries are keyboard-accessible without a mouse.
943 for (int i = 0; i < numRecentFiles; ++i) {
944 const QString text = "&" + QString::number(i + 1) + " " + QFileInfo(files.at(i)).fileName();
945 actions.at(i)->setText(text);
946 actions.at(i)->setData(files.at(i));
947 actions.at(i)->setVisible(true);
948 }
949
950 for (int i = numRecentFiles; i < RecentFiles::maxFiles; ++i) {
951 actions.at(i)->setVisible(false);
952 }
953}
954
955void MainWindow::openRecentFile()
956{
957 if (auto *action = qobject_cast<QAction *>(sender())) {
958 sentryBreadcrumb("file", QStringLiteral("Open recent file"));
959 loadPandaFile(action->data().toString());
960 }
961}
962
963void MainWindow::createRecentFileActions()
964{
965 m_ui->menuRecentFiles->clear();
966
967 for (int i = 0; i < RecentFiles::maxFiles; ++i) {
968 auto *action = new QAction(this);
969 action->setVisible(false);
970 connect(action, &QAction::triggered, this, &MainWindow::openRecentFile);
971 m_ui->menuRecentFiles->addAction(action);
972 }
973
974 updateRecentFileActions();
975}
976
978{
979 m_ui->retranslateUi();
980 m_ui->elementEditor->retranslateUi();
981 m_palette->retranslateLabels();
982
983 for (int index = 0; index < m_ui->tab->count(); ++index) {
984 auto *workspace = qobject_cast<WorkSpace *>(m_ui->tab->widget(index));
985 if (!workspace) {
986 continue;
987 }
988 auto *scene = workspace->scene();
989 if (!scene) {
990 continue;
991 }
992 auto *undoStack = scene->undoStack();
993 if (!undoStack) {
994 continue;
995 }
996 QString text;
997 if (workspace->isInlineIC()) {
998 text = "[" + workspace->inlineBlobName() + "]";
999 } else {
1000 auto fileInfo = workspace->fileInfo();
1001 text = fileInfo.exists() ? fileInfo.fileName() : tr("New Project");
1002 }
1003
1004 if (!undoStack->isClean()) {
1005 text += "*";
1006 }
1007
1008 m_ui->tab->setTabText(index, text);
1009
1010 scene->retranslateUi();
1011
1012 for (auto *elm : workspace->scene()->elements()) {
1013 elm->retranslate();
1014 }
1015 }
1016
1017 if (m_exerciseEngine->isActive()) {
1018 m_exerciseEngine->retranslate();
1019 }
1020 if (m_tourEngine->isActive()) {
1021 m_tourEngine->retranslate();
1022 }
1023}
1024
1025void MainWindow::loadTranslation(const QString &language)
1026{
1027 m_languageManager->loadTranslation(language);
1028}
1029
1031{
1032 m_ui->menuLanguage->clear();
1033
1034 auto *languageGroup = new QActionGroup(this);
1035 languageGroup->setExclusive(true);
1036
1037 for (const QString &langCode : m_languageManager->availableLanguages()) {
1038 auto *action = new QAction(m_languageManager->displayName(langCode), this);
1039 action->setCheckable(true);
1040 action->setData(langCode);
1041 action->setIcon(QIcon(m_languageManager->flagIcon(langCode)));
1042
1043 if (langCode == Settings::language() || (langCode == "en" && Settings::language().isEmpty())) {
1044 action->setChecked(true);
1045 }
1046
1047 languageGroup->addAction(action);
1048 m_ui->menuLanguage->addAction(action);
1049
1050 connect(action, &QAction::triggered, this, [this, langCode]() {
1051 m_languageManager->loadTranslation(langCode);
1052 });
1053 }
1054}
1055
1056void MainWindow::on_actionPlay_toggled(const bool checked)
1057{
1058 sentryBreadcrumb("simulation", QStringLiteral("Play toggled: %1").arg(checked));
1059 if (!currentTab()) {
1060 return;
1061 }
1062
1063 auto *simulation = currentTab()->simulation();
1064
1065 // The action is checkable; its toggled(bool) signal drives start/stop directly.
1066 checked ? simulation->start() : simulation->stop();
1067}
1068
1069void MainWindow::on_actionRestart_triggered()
1070{
1071 Application::guardedSlot(this, [this] {
1072 sentryBreadcrumb("simulation", QStringLiteral("Simulation restart"));
1073 if (!currentTab()) {
1074 return;
1075 }
1076
1078 });
1079}
1080
1081void MainWindow::populateMenu(QSpacerItem *spacer, const QStringList &names, QLayout *layout)
1082{
1083 // The spacer must be removed before inserting widgets so it stays at the
1084 // bottom after they are added; it is put back at the end of this function.
1085 layout->removeItem(spacer);
1086
1087 // Each element type gets two labels: one in its own category panel and a
1088 // duplicate in the shared search panel so search can find everything in one place.
1089 for (const auto &name : names) {
1090 auto type = ElementFactory::textToType(name);
1091 auto pixmap(ElementFactory::pixmap(type));
1092 layout->addWidget(new ElementLabel(pixmap, type, name, this));
1093 m_ui->scrollAreaWidgetContents_Search->layout()->addWidget(new ElementLabel(pixmap, type, name, this));
1094 }
1095
1096 layout->addItem(spacer);
1097}
1098
1099void MainWindow::on_actionFastMode_triggered(const bool checked)
1100{
1101 Application::guardedSlot(this, [this, checked] {
1102 sentryBreadcrumb("ui", QStringLiteral("Fast mode: %1").arg(checked));
1103 setFastMode(checked);
1104 Settings::setFastMode(checked);
1105 });
1106}
1107
1108void MainWindow::on_actionWaveform_triggered()
1109{
1110 Application::guardedSlot(this, [this] {
1111 if (m_bwd) {
1112 m_bwd->raise();
1113 m_bwd->activateWindow();
1114 return;
1115 }
1116 if (!currentTab()) {
1117 return;
1118 }
1119
1120 sentryBreadcrumb("ui", QStringLiteral("Waveform dialog opened"));
1121 qCDebug(zero) << "BD fileName: " << currentTab()->dolphinFileName();
1122 auto *bwd = new BewavedDolphin(currentTab()->scene(), true, this, this);
1123 bwd->createWaveform(currentTab()->dolphinFileName());
1124 m_bwd = bwd;
1125 connect(bwd, &QObject::destroyed, this, [this] {
1126 if (m_exerciseOverlay && m_exerciseEngine && m_exerciseEngine->isActive()) {
1127 m_exerciseOverlay->hide();
1128 m_exerciseOverlay->setParent(nullptr);
1129 }
1130 });
1131 bwd->show();
1132 });
1133}
1134
1135void MainWindow::on_actionLightTheme_triggered()
1136{
1137 Application::guardedSlot(qApp, [] {
1138 sentryBreadcrumb("ui", QStringLiteral("Theme: light"));
1140 });
1141}
1142
1143void MainWindow::on_actionDarkTheme_triggered()
1144{
1145 Application::guardedSlot(qApp, [] {
1146 sentryBreadcrumb("ui", QStringLiteral("Theme: dark"));
1148 });
1149}
1150
1151void MainWindow::on_actionSystemTheme_triggered()
1152{
1153 Application::guardedSlot(qApp, [] {
1154 sentryBreadcrumb("ui", QStringLiteral("Theme: system"));
1156 });
1157}
1158
1159void MainWindow::updateTheme()
1160{
1161 switch (ThemeManager::theme()) {
1162 case Theme::Dark: m_ui->actionDarkTheme->setChecked(true); break;
1163 case Theme::Light: m_ui->actionLightTheme->setChecked(true); break;
1164 case Theme::System: m_ui->actionSystemTheme->setChecked(true); break;
1165 }
1166
1167 m_palette->updateTheme();
1168 m_ui->elementEditor->updateTheme();
1169}
1170
1171void MainWindow::on_actionFlipHorizontally_triggered()
1172{
1173 Application::guardedSlot(this, [this] {
1174 if (!currentTab()) {
1175 return;
1176 }
1177
1179 });
1180}
1181
1182void MainWindow::on_actionFlipVertically_triggered()
1183{
1184 Application::guardedSlot(this, [this] {
1185 if (!currentTab()) {
1186 return;
1187 }
1188
1190 });
1191}
1192
1193void MainWindow::on_actionAlignLeft_triggered()
1194{
1195 Application::guardedSlot(this, [this] {
1196 if (!currentTab()) {
1197 return;
1198 }
1199
1200 currentTab()->scene()->alignLeft();
1201 });
1202}
1203
1204void MainWindow::on_actionAlignRight_triggered()
1205{
1206 Application::guardedSlot(this, [this] {
1207 if (!currentTab()) {
1208 return;
1209 }
1210
1211 currentTab()->scene()->alignRight();
1212 });
1213}
1214
1215void MainWindow::on_actionAlignTop_triggered()
1216{
1217 Application::guardedSlot(this, [this] {
1218 if (!currentTab()) {
1219 return;
1220 }
1221
1222 currentTab()->scene()->alignTop();
1223 });
1224}
1225
1226void MainWindow::on_actionAlignBottom_triggered()
1227{
1228 Application::guardedSlot(this, [this] {
1229 if (!currentTab()) {
1230 return;
1231 }
1232
1234 });
1235}
1236
1237void MainWindow::on_actionAlignHorizontalCenter_triggered()
1238{
1239 Application::guardedSlot(this, [this] {
1240 if (!currentTab()) {
1241 return;
1242 }
1243
1245 });
1246}
1247
1248void MainWindow::on_actionAlignVerticalCenter_triggered()
1249{
1250 Application::guardedSlot(this, [this] {
1251 if (!currentTab()) {
1252 return;
1253 }
1254
1256 });
1257}
1258
1259void MainWindow::on_actionDistributeHorizontally_triggered()
1260{
1261 Application::guardedSlot(this, [this] {
1262 if (!currentTab()) {
1263 return;
1264 }
1265
1267 });
1268}
1269
1270void MainWindow::on_actionDistributeVertically_triggered()
1271{
1272 Application::guardedSlot(this, [this] {
1273 if (!currentTab()) {
1274 return;
1275 }
1276
1278 });
1279}
1280
1282{
1283 return m_workspaceManager->dolphinFileName();
1284}
1285
1286void MainWindow::setDolphinFileName(const QString &fileName)
1287{
1288 m_workspaceManager->setDolphinFileName(fileName);
1289}
1290
1291void MainWindow::on_actionFullscreen_triggered()
1292{
1293 Application::guardedSlot(this, [this] {
1294 sentryBreadcrumb("ui", QStringLiteral("Fullscreen toggled"));
1295 isFullScreen() ? showNormal() : showFullScreen();
1296 });
1297}
1298
1299void MainWindow::on_actionMute_triggered(const bool checked)
1300{
1301 Application::guardedSlot(this, [this, checked] {
1302 sentryBreadcrumb("ui", QStringLiteral("Mute toggled"));
1303 if (!currentTab()) {
1304 return;
1305 }
1306
1307 currentTab()->simulation()->setUserMuted(checked);
1308 m_ui->actionMute->setText(checked ? tr("Unmute") : tr("Mute"));
1309 });
1310}
1311
1312void MainWindow::on_actionLabelsUnderIcons_triggered(const bool checked)
1313{
1314 Application::guardedSlot(this, [this, checked] {
1315 sentryBreadcrumb("ui", QStringLiteral("Labels under icons: %1").arg(checked));
1316 m_ui->mainToolBar->setToolButtonStyle(checked ? Qt::ToolButtonTextUnderIcon : Qt::ToolButtonIconOnly);
1318 });
1319}
1320
1321void MainWindow::on_actionICPreview_triggered(const bool checked)
1322{
1323 Application::guardedSlot(this, [checked] {
1324 sentryBreadcrumb("ui", QStringLiteral("IC preview: %1").arg(checked));
1326 });
1327}
1328
1329void MainWindow::on_actionCheckForUpdates_triggered(const bool checked)
1330{
1331 Application::guardedSlot(this, [checked] {
1332 sentryBreadcrumb("ui", QStringLiteral("Auto update checks: %1").arg(checked));
1334 });
1335}
1336
1337void MainWindow::on_actionShowMinimap_triggered(const bool checked)
1338{
1339 Application::guardedSlot(this, [this, checked] {
1340 sentryBreadcrumb("ui", QStringLiteral("Show minimap: %1").arg(checked));
1342 if (currentTab()) currentTab()->setMinimapVisible(checked);
1343 });
1344}
1345
1347{
1348 switch (event->type()) {
1349 case QEvent::WindowActivate: {
1350 // Resume simulation when the window regains focus.
1351 if (m_ui->actionPlay->isChecked()) {
1352 on_actionPlay_toggled(true);
1353 }
1354 break;
1355 }
1356
1357 case QEvent::WindowDeactivate: {
1358 // Pause simulation when the window loses focus unless the user opted in
1359 // to background simulation (useful for demoing circuits on a second display).
1360 if (!m_ui->actionBackground_Simulation->isChecked()) {
1361 on_actionPlay_toggled(false);
1362 }
1363 break;
1364 }
1365
1366 default: break;
1367 };
1368
1369 return QMainWindow::event(event);
1370}
1371
1372void MainWindow::startExercise(const QString &resourcePath)
1373{
1374 if (!currentTab() || resourcePath.isEmpty()) {
1375 return;
1376 }
1377 if (!m_exerciseEngine->loadFromResource(resourcePath)) {
1378 qWarning() << "ExerciseEngine: failed to load" << resourcePath;
1379 return;
1380 }
1381
1382 if (currentTab()) {
1383 currentTab()->setExerciseOverlay(nullptr);
1384 }
1385 delete m_exerciseOverlay;
1386 m_exerciseOverlay = new ExerciseOverlay(m_exerciseEngine, currentTab());
1387
1388 // m_exerciseEngine outlives any single exercise run, so a prior startExercise() call's
1389 // click-handling connection (below) must be dropped before reconnecting, or replaying an
1390 // exercise fires accumulated handlers once per past run.
1391 disconnect(m_exerciseEngine, &ExerciseEngine::stepChanged, this, nullptr);
1392
1393 // Must precede stepChanged→onStepChanged so actions run before the overlay updates.
1394 connect(m_exerciseEngine, &ExerciseEngine::stepChanged, this,
1395 [this](int, int, const ExerciseStep &step) {
1396 for (const QString &id : step.click) clickTarget(id);
1397
1398 if (!m_exerciseOverlay) {
1399 return;
1400 }
1401 const bool wantBwd = (step.context == "bwd");
1402 if (wantBwd && m_bwd) {
1403 m_exerciseOverlay->setParent(m_bwd->centralWidget());
1404 m_bwd->setExerciseOverlay(m_exerciseOverlay);
1405 if (currentTab()) {
1406 currentTab()->setExerciseOverlay(nullptr);
1407 }
1408 } else if (!wantBwd && currentTab()) {
1409 m_exerciseOverlay->setParent(currentTab());
1410 currentTab()->setExerciseOverlay(m_exerciseOverlay);
1411 if (m_bwd) {
1412 m_bwd->setExerciseOverlay(nullptr);
1413 }
1414 }
1415 m_exerciseOverlay->repositionToParent();
1416 m_exerciseOverlay->show();
1417 m_exerciseOverlay->raise();
1418 });
1419 connect(m_exerciseEngine, &ExerciseEngine::stepChanged,
1420 m_exerciseOverlay, &ExerciseOverlay::onStepChanged);
1421 connect(m_exerciseEngine, &ExerciseEngine::exerciseCompleted,
1422 m_exerciseOverlay, &ExerciseOverlay::onExerciseCompleted);
1423 connect(m_exerciseEngine, &ExerciseEngine::retranslated,
1424 m_exerciseOverlay, &ExerciseOverlay::onRetranslated);
1425 connect(m_exerciseOverlay, &ExerciseOverlay::closeRequested, this, [this] {
1426 if (!m_exerciseOverlay) return;
1427 ExerciseOverlay *overlay = m_exerciseOverlay;
1428 m_exerciseOverlay = nullptr;
1429 m_exerciseEngine->stop();
1430 if (currentTab()) {
1431 currentTab()->setExerciseOverlay(nullptr);
1432 }
1433 if (m_bwd) {
1434 m_bwd->setExerciseOverlay(nullptr);
1435 }
1436 overlay->deleteLater();
1437 });
1438
1439 auto *esc = new QShortcut(QKeySequence(Qt::Key_Escape), m_exerciseOverlay);
1440 connect(esc, &QShortcut::activated, m_exerciseOverlay, &ExerciseOverlay::closeRequested);
1441
1442 currentTab()->setExerciseOverlay(m_exerciseOverlay);
1443 m_exerciseEngine->setScene(currentTab()->scene());
1444 m_exerciseEngine->start();
1445
1446 m_exerciseOverlay->repositionToParent();
1447 m_exerciseOverlay->show();
1448 m_exerciseOverlay->raise();
1449}
1450
1451void MainWindow::startTour(const QString &resourcePath)
1452{
1453 if (resourcePath.isEmpty()) {
1454 return;
1455 }
1456 if (!m_tourEngine->loadFromResource(resourcePath)) {
1457 qWarning() << "TourEngine: failed to load" << resourcePath;
1458 return;
1459 }
1460
1461 delete m_tourOverlay;
1462 m_tourOverlay = new TourOverlay(m_tourEngine, this);
1463 m_tourOverlay->setTargetResolver([this](const QString &id) {
1464 return resolveTourTarget(id);
1465 });
1466
1467 // m_tourEngine outlives any single tour run, so a prior startTour() call's click-handling
1468 // connection (below) must be dropped before reconnecting, or replaying a tour fires
1469 // accumulated handlers once per past run.
1470 disconnect(m_tourEngine, &TourEngine::stepChanged, this, nullptr);
1471
1472 // Must be connected before stepChanged→onStepChanged so click executes before
1473 // resolveTourTarget computes widget rects.
1474 connect(m_tourEngine, &TourEngine::stepChanged, this,
1475 [this](int, int, const TourStep &step) {
1476 for (const QString &id : step.click) clickTarget(id);
1477
1478 TourOverlay *overlay = m_tourOverlay;
1479 if (!overlay) {
1480 return;
1481 }
1482 const bool wantBwd = step.target.startsWith("bwd:");
1483 if (wantBwd && m_bwd) {
1484 if (overlay->parentWidget() != m_bwd) {
1485 overlay->setParentWindow(m_bwd);
1486 }
1487 overlay->show();
1488 overlay->raise();
1489 } else if (!wantBwd && overlay->parentWidget() != this) {
1490 overlay->setParentWindow(this);
1491 overlay->show();
1492 overlay->raise();
1493 }
1494 });
1495 connect(m_tourEngine, &TourEngine::stepChanged,
1496 m_tourOverlay, &TourOverlay::onStepChanged);
1497 connect(m_tourEngine, &TourEngine::tourCompleted,
1498 m_tourOverlay, &TourOverlay::onTourFinished);
1499 connect(m_tourEngine, &TourEngine::tourStopped,
1500 m_tourOverlay, &TourOverlay::onTourFinished);
1501 connect(m_tourEngine, &TourEngine::retranslated,
1502 m_tourOverlay, &TourOverlay::onRetranslated);
1503 connect(m_tourOverlay, &TourOverlay::closeRequested, this, [this] {
1504 if (!m_tourOverlay) return;
1505 TourOverlay *overlay = m_tourOverlay;
1506 m_tourOverlay = nullptr;
1507 m_tourEngine->stop();
1508 overlay->deleteLater();
1509 });
1510
1511 auto *esc = new QShortcut(QKeySequence(Qt::Key_Escape), m_tourOverlay);
1512 connect(esc, &QShortcut::activated, m_tourOverlay, &TourOverlay::closeRequested);
1513
1514 m_tourEngine->start();
1515
1516 m_tourOverlay->show();
1517 m_tourOverlay->raise();
1518}
1519
1520QRect MainWindow::resolveTourTarget(const QString &id) const
1521{
1522 if (!m_tourOverlay || id.isEmpty() || id == "none") {
1523 return {};
1524 }
1525
1526 auto mapWidget = [this](QWidget *w) -> QRect {
1527 if (!w) return {};
1528 const QPoint topLeft = m_tourOverlay->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)));
1529 return QRect(topLeft, w->size());
1530 };
1531
1532 if (id == "toolbar") return mapWidget(m_ui->mainToolBar->widgetForAction(m_ui->actionWaveform));
1533 if (id == "elementPalette") return mapWidget(m_ui->tabElements);
1534 if (id == "gatesTab") {
1535 QTabBar *bar = m_ui->tabElements->tabBar();
1536 const QRect tabRect = bar->tabRect(1);
1537 const QPoint topLeft = m_tourOverlay->mapFromGlobal(bar->mapToGlobal(tabRect.topLeft()));
1538 return QRect(topLeft, tabRect.size());
1539 }
1540 if (id == "ioTab") {
1541 QTabBar *bar = m_ui->tabElements->tabBar();
1542 const QRect tabRect = bar->tabRect(0);
1543 const QPoint topLeft = m_tourOverlay->mapFromGlobal(bar->mapToGlobal(tabRect.topLeft()));
1544 return QRect(topLeft, tabRect.size());
1545 }
1546 if (id == "canvasArea" && currentTab()) return mapWidget(currentTab()->view());
1547 if (id == "elementEditor") return mapWidget(m_ui->elementEditor);
1548 if (id == "searchBar") return mapWidget(m_ui->lineEditSearch);
1549
1550 BewavedDolphin *bwd = m_bwd;
1551 if (id.startsWith("bwd:") && bwd) {
1552 auto mapBwd = [this](QWidget *w) -> QRect {
1553 if (!w || !m_tourOverlay) {
1554 return {};
1555 }
1556 const QPoint tl = m_tourOverlay->mapFromGlobal(w->mapToGlobal(QPoint(0, 0)));
1557 return QRect(tl, w->size());
1558 };
1559 const QString sub = id.sliced(4);
1560 if (sub == "tableView") return mapBwd(bwd->signalTableView());
1561 if (sub == "toolbar") return mapBwd(bwd->mainToolBar()->widgetForAction(bwd->actionCombinational()));
1562 if (sub == "menuBar") return mapBwd(bwd->menuBar());
1563 return {};
1564 }
1565
1566 return {};
1567}
1568
1569void MainWindow::clickTarget(const QString &id)
1570{
1571 if (id == "ioTab") m_ui->tabElements->setCurrentIndex(0);
1572 else if (id == "gatesTab") m_ui->tabElements->setCurrentIndex(1);
1573 else if (id == "combinational") m_ui->tabElements->setCurrentIndex(2);
1574 else if (id == "memoryTab") m_ui->tabElements->setCurrentIndex(3);
1575 else if (id == "actionPlay") m_ui->actionPlay->trigger();
1576 else if (id == "actionWaveform") m_ui->actionWaveform->trigger();
1577 else if (id == "bwd:actionCombinational" && m_bwd) m_bwd->triggerCombinational();
1578 else if (id == "setupElementEditorDemo") {
1579 if (!currentTab()) {
1580 return;
1581 }
1582 Scene *scene = currentTab()->scene();
1583 scene->clearSelection();
1584 auto *sw = ElementFactory::buildElement(ElementType::InputSwitch);
1585 sw->setPos(0, 0);
1586 // AddItemsCommand registers the element with the scene's Simulation
1587 // (setCircuitUpdateRequired() in redo()) and selects it for us.
1588 scene->receiveCommand(new AddItemsCommand({sw}, scene));
1589 }
1590 else if (id == "setupWaveformDemo") {
1591 if (!currentTab()) {
1592 return;
1593 }
1594 Scene *scene = currentTab()->scene();
1595 scene->clearSelection();
1596 auto *clock1 = ElementFactory::buildElement(ElementType::Clock);
1597 auto *clock2 = ElementFactory::buildElement(ElementType::Clock);
1598 auto *gate = ElementFactory::buildElement(ElementType::And);
1599 auto *led = ElementFactory::buildElement(ElementType::Led);
1600 clock1->setPos(-160, -60);
1601 clock2->setPos(-160, 60);
1602 gate->setPos(0, 0);
1603 led->setPos(160, 0);
1604
1605 auto *conn1 = new Connection();
1606 conn1->setStartPort(clock1->outputPort(0));
1607 conn1->setEndPort(gate->inputPort(0));
1608 auto *conn2 = new Connection();
1609 conn2->setStartPort(clock2->outputPort(0));
1610 conn2->setEndPort(gate->inputPort(1));
1611 auto *conn3 = new Connection();
1612 conn3->setStartPort(gate->outputPort(0));
1613 conn3->setEndPort(led->inputPort(0));
1614
1615 // AddItemsCommand auto-discovers conn1..conn3 via port traversal (loadList())
1616 // and registers all 7 items with the scene's Simulation, fixing the
1617 // frozen-clock bug that raw scene->addItem() caused.
1618 scene->receiveCommand(new AddItemsCommand({clock1, clock2, gate, led}, scene));
1619
1620 conn1->updatePath();
1621 conn2->updatePath();
1622 conn3->updatePath();
1623 }
1624}
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.
Draggable element icon+label widget used in the element palette.
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:73
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 QPixmap pixmap(const ElementType type)
Returns the pixmap icon for the given element type.
static ElementType textToType(const QString &text)
Converts an element type name string to its ElementType enum value.
static GraphicElement * buildElement(const ElementType type)
Constructs and returns a new graphic element of the given type.
Icon and name widget shown in the left-panel element palette.
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 removeICFile(const QString &icFileName)
Deletes icFileName from the project directory and any instances of it in the scene.
void addICFromFile()
Copies a chosen .panda file (and its dependencies) into the project's IC directory.
void embedSelectedIC()
Embeds the currently selected file-backed IC as a blob in the circuit.
void removeEmbeddedIC(const QString &blobName)
Removes the embedded IC blob blobName from the current circuit.
void showRemoveICHint()
Shows the "drag here to remove" hint for the file-based IC trash button.
void extractSelectedIC()
Extracts the currently selected embedded IC back out to a .panda file.
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 populateMenu(QSpacerItem *spacer, const QStringList &names, QLayout *layout)
Fills layout with ElementLabel buttons from names, preceded by spacer.
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