35 m_tabBar = m_tab->tabBar();
36 m_tabBar->installEventFilter(
this);
41 if (watched == m_tabBar && event->type() == QEvent::MouseButtonRelease) {
42 const auto *mouseEvent =
static_cast<QMouseEvent *
>(event);
43 if (mouseEvent->button() == Qt::MiddleButton) {
44 const int index = m_tabBar->tabAt(mouseEvent->pos());
51 return QObject::eventFilter(watched, event);
56 return m_currentTab ? m_currentTab->fileInfo() : QFileInfo();
59QString WorkspaceManager::displayName(
const WorkSpace *ws,
const QFileInfo &fileInfo)
const
68 if (fileInfo.exists()) {
69 return fileInfo.fileName();
74 QString name = ws->
untitledTitle().isEmpty() ? tr(
"New Project") : ws->untitledTitle();
76 name += tr(
" (recovered)");
88 return m_currentTab ? m_currentTab->fileInfo().absoluteDir() : QDir();
95 auto *ws = m_currentTab;
110 return m_currentTab->dolphinFileName();
118 m_currentTab->setDolphinFileName(fileName);
121QString WorkspaceManager::nextUntitledTitle()
const
125 taken.reserve(m_tab->count());
126 for (
int i = 0; i < m_tab->count(); ++i) {
127 QString title = m_tab->tabText(i);
128 if (title.endsWith(QLatin1Char(
'*'))) {
134 const QString base = tr(
"New Project");
135 if (!taken.contains(base)) {
138 for (
int n = 2;; ++n) {
139 const QString candidate = tr(
"New Project %1").arg(n);
140 if (!taken.contains(candidate)) {
148 qCDebug(zero) <<
"Creating new workspace.";
149 auto *workspace =
new WorkSpace(m_host.widget());
154 workspace->scene()->updateTheme();
157 const QString untitledTitle = nextUntitledTitle();
158 workspace->setUntitledTitle(untitledTitle);
160 qCDebug(zero) <<
"Adding tab. #tabs: " << m_tab->count() <<
", current tab: " << m_tabIndex;
161 m_tab->addTab(workspace, untitledTitle);
164 qCDebug(zero) <<
"Selecting the newly created tab.";
166 m_tab->setCurrentIndex(m_tab->count() - 1);
169QString WorkspaceManager::promptSavePath(
const QString &fileName)
171 QString resolved = fileName.isEmpty() ?
currentFile().absoluteFilePath() : fileName;
175 if ((resolved.isEmpty() || autosaves.contains(resolved)) &&
currentFile().fileName().isEmpty()) {
176 const QString path = resolved.isEmpty() ?
currentFile().absolutePath() : QFileInfo(resolved).absolutePath();
180 if (resolved.isEmpty()) {
184 if (!resolved.endsWith(
".panda")) {
185 resolved.append(
".panda");
197 if (m_currentTab->isInlineIC()) {
199 m_currentTab->save(fileName);
201 m_host.showStatusMessage(tr(
"File saved successfully."), 4000);
208 const QString originalFileName = fileName.isEmpty() ?
currentFile().absoluteFilePath() : fileName;
209 const bool wasAutosaveRecord = !originalFileName.isEmpty() &&
Settings::autosaveFiles().contains(originalFileName);
211 QString resolvedFileName = promptSavePath(fileName);
212 if (resolvedFileName.isEmpty()) {
217 const auto outcome = m_currentTab->save(resolvedFileName);
228 m_host.widget(), tr(
"Save File (original location is read-only)"),
229 QFileInfo(resolvedFileName).fileName(),
230 tr(
"Panda files") +
" (*.panda)").
fileName;
231 if (newPath.isEmpty()) {
234 resolvedFileName = newPath.endsWith(
".panda") ? newPath : newPath +
".panda";
237 if (wasAutosaveRecord) {
239 autosaves.removeAll(originalFileName);
244 m_host.showStatusMessage(tr(
"File saved successfully."), 4000);
247int WorkspaceManager::closeTabAnyway()
250 msgBox.setParent(m_host.
widget());
251 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
252 msgBox.setText(tr(
"File not saved. Close tab anyway?"));
253 msgBox.setWindowModality(Qt::WindowModal);
254 msgBox.setDefaultButton(QMessageBox::No);
255 return msgBox.exec();
261 msgBox.setParent(m_host.widget());
266 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel);
268 msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
271 const QString fileName =
currentFile().fileName().isEmpty() ? tr(
"New Project") :
currentFile().fileName();
273 msgBox.setText(fileName + tr(
" has been modified.\nDo you want to save your changes?"));
274 msgBox.setWindowModality(Qt::WindowModal);
275 msgBox.setDefaultButton(QMessageBox::Yes);
276 return msgBox.exec();
289 const QFileInfo newFileInfo(fileName);
291 for (
int i = 0; i < m_tab->count(); ++i) {
292 if (
auto *workspace = qobject_cast<WorkSpace *>(m_tab->widget(i))) {
293 if (workspace->fileInfo() == newFileInfo) {
294 m_tab->setCurrentIndex(i);
301 qCDebug(zero) <<
"Loading in editor.";
303 m_currentTab->load(fileName);
308 m_currentTab->scene()->undoStack()->clear();
314 m_currentTab->scene()->resizeScene();
316 m_host.palette()->updateEmbeddedICList(m_currentTab->scene());
317 m_host.showStatusMessage(tr(
"File loaded successfully."), 4000);
327 for (
int i = 0; i < m_tab->count(); ++i) {
328 auto *ws = qobject_cast<WorkSpace *>(m_tab->widget(i));
331 m_tab->setCurrentIndex(i);
336 auto *parentWorkspace = m_currentTab;
340 m_currentTab->loadFromBlob(blob, parentWorkspace, icElementId, parentWorkspace->scene()->contextDir());
343 m_currentTab->scene()->resizeScene();
346 m_host.setICButtonsVisible(
false);
349 int tabIndex = m_tab->indexOf(m_currentTab);
350 m_tab->setTabText(tabIndex,
"[" + blobName +
"]");
353 m_host.palette()->updateEmbeddedICList(m_currentTab->scene());
359void WorkspaceManager::onBlobRenamed(
const QString &oldName,
const QString &newName)
361 for (
int i = 0; i < m_tab->count(); ++i) {
362 auto *ws = qobject_cast<WorkSpace *>(m_tab->widget(i));
365 m_tab->setTabText(i,
"[" + newName +
"]");
375 auto fileContentReady = [
this](
const QString &fileName,
const QByteArray &fileContent) {
376 if (!fileName.isEmpty()) {
378 QFile file(fileName);
379 if (!file.open(QIODevice::WriteOnly)) {
382 if (file.write(fileContent) != fileContent.size()) {
390 QFileDialog::getOpenFileContent(
"Panda files (*.panda)", fileContentReady);
394 if (fileName.isEmpty()) {
412 if (m_currentTab->isInlineIC()) {
423 QString fileName =
currentFile().absoluteFilePath();
425 if (fileName.isEmpty()) {
428 if (fileName.isEmpty()) {
432 if (!fileName.endsWith(
".panda")) {
433 fileName.append(
".panda");
437 if (warnIfOpenInAnotherTab(fileName)) {
456 const QString tmpPath =
"/tmp/wiredpanda_save.panda";
460 if (file.open(QIODevice::ReadOnly)) {
461 const QByteArray content = file.readAll();
465 if (suggestedName.isEmpty()) {
466 suggestedName =
"circuit.panda";
468 QFileDialog::saveFileContent(content, suggestedName);
473 if (fileName.isEmpty()) {
477 if (!fileName.endsWith(
".panda")) {
478 fileName.append(
".panda");
481 if (warnIfOpenInAnotherTab(fileName)) {
498 const QString file =
currentFile().absoluteFilePath();
505int WorkspaceManager::findTabWithFile(
const QString &fileName)
const
507 const QFileInfo newFileInfo(fileName);
508 for (
int i = 0; i < m_tab->count(); ++i) {
509 if (
auto *workspace = qobject_cast<WorkSpace *>(m_tab->widget(i))) {
510 if (workspace != m_currentTab && workspace->fileInfo() == newFileInfo) {
518bool WorkspaceManager::warnIfOpenInAnotherTab(
const QString &fileName)
520 const int conflictTab = findTabWithFile(fileName);
521 if (conflictTab == -1) {
525 QMessageBox msgBox(QMessageBox::Warning,
527 tr(
"The file \"%1\" is already open in another tab.").arg(QFileInfo(fileName).fileName()),
530 QPushButton *switchBtn = msgBox.addButton(tr(
"Switch to Tab"), QMessageBox::ActionRole);
532 if (msgBox.clickedButton() == switchBtn) {
533 m_tab->setCurrentIndex(conflictTab);
541 while (m_tab->count() != 0) {
542 if (!
closeTab(m_tab->count() - 1)) {
553 const auto workspaces = m_tab->findChildren<
WorkSpace *>();
555 for (
auto *workspace : workspaces) {
556 auto *scene = workspace->scene();
561 auto *undoStack = scene->undoStack();
567 if (!undoStack->isClean()) {
575 const QString filePath = workspace->fileInfo().absoluteFilePath();
577 if (!filePath.isEmpty() && autosaves.contains(filePath)) {
589 qCDebug(zero) <<
"All autosave files: " << autosaves;
591 for (
auto it = autosaves.begin(); it != autosaves.end();) {
594 if (!file.exists()) {
595 qCDebug(zero) <<
"Removing from config the autosave file that does not exist.";
596 it = autosaves.erase(it);
602 }
catch (
const std::exception &e) {
604 QMessageBox::critical(
nullptr, tr(
"Error!"), e.what());
606 qCDebug(zero) <<
"Removing autosave file that is corrupted.";
607 it = autosaves.erase(it);
613 m_currentTab->setAutosaveFile();
615 m_currentTab->setRecovered(
true);
628 auto *senderWs = qobject_cast<WorkSpace *>(sender());
630 senderWs = m_currentTab;
636 const int tabIndex = m_tab->indexOf(senderWs);
641 QString text = displayName(senderWs, fileInfo);
645 auto *scene = senderWs->scene();
647 auto *undoStack = scene->undoStack();
648 if (undoStack && !undoStack->isClean()) {
653 m_tab->setTabText(tabIndex, text);
656 if (senderWs == m_currentTab) {
661 if (!senderWs->isInlineIC()) {
662 m_tab->setTabToolTip(tabIndex, fileInfo.absoluteFilePath());
663 qCDebug(zero) <<
"Adding file to recent files.";
667 if (senderWs == m_currentTab) {
668 m_host.refreshICButtonsEnabled();
674 qCDebug(zero) <<
"Closing tab " << tabIndex + 1 <<
", #tabs: " << m_tab->count();
677 m_tab->setCurrentIndex(tabIndex);
679 qCDebug(zero) <<
"Checking if needs to save file.";
681 bool needsSave =
false;
683 auto *scene = m_currentTab->scene();
685 auto *undoStack = scene->undoStack();
686 needsSave = undoStack && !undoStack->isClean();
693 if (selectedButton == QMessageBox::Cancel) {
697 if (selectedButton == QMessageBox::Yes) {
700 }
catch (
const std::exception &e) {
701 QMessageBox::critical(m_host.widget(), tr(
"Error"), e.what());
704 if (closeTabAnyway() == QMessageBox::No) {
711 qCDebug(zero) <<
"Deleting tab.";
712 m_currentTab->deleteLater();
714 m_tab->removeTab(tabIndex);
716 qCDebug(zero) <<
"Closed tab " << tabIndex <<
", #tabs: " << m_tab->count() <<
", current tab: " << m_tabIndex;
723 sentryBreadcrumb(
"ui", QStringLiteral(
"Tab changed to index %1").arg(newIndex));
724 m_tabIndex = newIndex;
725 m_currentTab = (newIndex == -1) ?
nullptr : qobject_cast<WorkSpace *>(m_tab->currentWidget());
726 qCDebug(zero) <<
"Selecting tab: " << newIndex;
Custom QApplication subclass with exception handling and main-window access.
Common logging utilities, the Pandaception error type, and helper macros.
#define qCDebug(category)
ElementPalette: manages the left-panel element palette and search UI.
Abstract file dialog interface for testability.
IC definition registry with file watching and embedded blob storage.
MainWindowHost: the application context that MainWindow's extracted controllers depend on.
Main circuit editing scene with undo/redo and user interaction.
Lightweight Sentry helpers gated behind HAVE_SENTRY.
void sentryBreadcrumb(const char *category, const QString &message)
Typed wrappers around QSettings for all application preferences.
WorkspaceManager: owns the circuit tabs and their file lifecycle.
WorkSpace widget: the complete circuit editing environment for one tab.
static void guardedSlot(const QObject *context, Body &&body) noexcept
Wraps a slot body in try/catch and reports any exception synchronously, inside the slot's own stack f...
static bool interactiveMode
virtual FileDialogResult getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
virtual QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter)=0
void blobRenamed(const QString &oldName, const QString &newName)
Interface MainWindow provides to its extracted controllers (export, IC, …).
virtual QWidget * widget()=0
Widget used to parent modal dialogs spawned by a controller.
static QStringList autosaveFiles()
static void setAutosaveFiles(const QStringList &files)
A widget containing a complete circuit editing environment.
void icBlobSaved(int icElementId, const QByteArray &blob)
Emitted when an inline IC tab saves its blob (propagated to parent).
const QString & untitledTitle() const
bool isInlineIC() const
Returns true if this workspace is editing an embedded IC blob (not a file).
void fileChanged(const QFileInfo &fileInfo)
Emitted whenever the file info of this workspace changes (load/save).
const QString & inlineBlobName() const
Returns the blob name being edited (for tab title).
void onChildICBlobSaved(int icElementId, const QByteArray &blob)
Receives a saved blob from a child inline tab.
void setInlineBlobName(const QString &blobName)
WorkSpace * parentWorkspace() const
Returns the parent workspace (for inline IC tabs).
QFileInfo fileInfo() const
Returns the file info for the currently open circuit file.
void recentFileAdded(const QString &filePath)
Emitted when a file-backed tab is (re)named, to feed the recent-files list.
QString currentTabName() const
int confirmSave(bool multiple)
QFileInfo currentFile() const
bool closeTab(int tabIndex)
Closes the tab at tabIndex (prompting to save if needed). Returns false if cancelled.
void setDolphinFileName(const QString &fileName)
WorkspaceManager(QTabWidget *tab, MainWindowHost &host, QObject *parent=nullptr)
QString dolphinFileName() const
bool eventFilter(QObject *watched, QEvent *event) override
Closes the tab under the cursor when its tab bar is middle-clicked.
void openICInTab(const QString &blobName, int icElementId, const QByteArray &blob)
void onCurrentIndexChanged(int newIndex)
Reacts to QTabWidget::currentChanged: updates the current tab and emits currentTabChanged.
void save(const QString &fileName={})
void currentTabChanged(WorkSpace *tab)
Emitted when the active tab changes (or becomes null); the shell rebinds the chrome.
QFileInfo icListFile() const
void setCurrentFile(const QFileInfo &fileInfo)
Updates the tab title / tooltip / recent files for the workspace that emitted fileChanged.
void loadPandaFile(const QString &fileName)
FileDialogProvider * provider()
Returns the active provider. Never null.
QString fileName
Selected file path, empty if cancelled.