From cc7d3d045133f1b2f1ce15599c921a99ab45c019 Mon Sep 17 00:00:00 2001 From: legend-df <78252402+legend-df@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:21:03 +0800 Subject: [PATCH] Fix calculator build issues on Windows --- examples/calculator/CMakeLists.txt | 6 +- examples/calculator/FlowchartManager.cpp | 578 +++++++++++++++++- examples/calculator/FlowchartManager.hpp | 28 +- .../internal/DataFlowGraphicsScene.hpp | 4 + src/DataFlowGraphicsScene.cpp | 40 +- 5 files changed, 628 insertions(+), 28 deletions(-) diff --git a/examples/calculator/CMakeLists.txt b/examples/calculator/CMakeLists.txt index 527326926..5ebed6f35 100644 --- a/examples/calculator/CMakeLists.txt +++ b/examples/calculator/CMakeLists.txt @@ -10,16 +10,20 @@ set(CALC_HEADER_FILES AdditionModel.hpp DivisionModel.hpp DecimalData.hpp - FlowchartManager.hpp MathOperationDataModel.hpp NumberDisplayDataModel.hpp NumberSourceDataModel.hpp SubtractionModel.hpp ) +set(CALC_GUI_HEADER_FILES + FlowchartManager.hpp +) + add_executable(calculator ${CALC_SOURCE_FILES} ${CALC_HEADER_FILES} + ${CALC_GUI_HEADER_FILES} ) target_link_libraries(calculator QtNodes) diff --git a/examples/calculator/FlowchartManager.cpp b/examples/calculator/FlowchartManager.cpp index 0a58acd21..5f644073f 100644 --- a/examples/calculator/FlowchartManager.cpp +++ b/examples/calculator/FlowchartManager.cpp @@ -2,9 +2,23 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include +#include +#include #include #include #include @@ -62,22 +76,83 @@ FlowchartPage::FlowchartPage(std::shared_ptr bool FlowchartPage::save() { - if (_scene->save()) { - setModified(false); - return true; + if (_filePath.isEmpty()) + return saveAs(); + + return saveToPath(_filePath, false); +} + +bool FlowchartPage::saveAs() +{ + QString suggestedPath = _filePath; + if (suggestedPath.isEmpty()) { + suggestedPath = QDir::homePath(); + if (!_displayName.isEmpty()) + suggestedPath = QDir(suggestedPath).filePath(_displayName + QStringLiteral(".flow")); } - return false; + QString fileName = QFileDialog::getSaveFileName(this, + tr("Save Flowchart"), + suggestedPath, + tr("Flow Scene Files (*.flow)")); + + if (fileName.isEmpty()) + return false; + + if (!fileName.endsWith(QStringLiteral(".flow"), Qt::CaseInsensitive)) + fileName += QStringLiteral(".flow"); + + return saveToPath(fileName, true); +} + +bool FlowchartPage::saveToPath(QString const &fileName, bool updateDisplayName) +{ + if (!_scene->saveToFile(fileName)) + return false; + + _filePath = fileName; + + if (updateDisplayName) + setDisplayName(QFileInfo(fileName).completeBaseName()); + + setModified(false); + + return true; } bool FlowchartPage::load() { - if (_scene->load()) { - setModified(false); - return true; - } + QString directory = _filePath.isEmpty() ? QDir::homePath() : QFileInfo(_filePath).absolutePath(); + + QString fileName = QFileDialog::getOpenFileName(this, + tr("Open Flowchart"), + directory, + tr("Flow Scene Files (*.flow)")); + + if (fileName.isEmpty()) + return false; + + return loadFromFile(fileName); +} - return false; +bool FlowchartPage::loadFromFile(QString const &fileName, QString const &displayNameOverride) +{ + if (!_scene->loadFromFile(fileName)) + return false; + + _filePath = fileName; + if (displayNameOverride.isEmpty()) + setDisplayName(QFileInfo(fileName).completeBaseName()); + else + setDisplayName(displayNameOverride); + setModified(false); + + return true; +} + +bool FlowchartPage::hasContent() const +{ + return !_model.allNodeIds().empty(); } void FlowchartPage::setDisplayName(QString name) @@ -85,6 +160,14 @@ void FlowchartPage::setDisplayName(QString name) _displayName = std::move(name); } +void FlowchartPage::setFilePath(QString filePath, bool updateDisplayName) +{ + _filePath = std::move(filePath); + + if (updateDisplayName && !_filePath.isEmpty()) + setDisplayName(QFileInfo(_filePath).completeBaseName()); +} + void FlowchartPage::runFlowchart() { _model.runAllNodes(); @@ -117,6 +200,12 @@ FlowchartManager::FlowchartManager(std::shared_ptraddAction(tr("Save Flowchart")); _saveAction->setShortcut(QKeySequence::Save); + _saveAsAction = menu->addAction(tr("Save Flowchart As...")); + _saveAsAction->setShortcut(QKeySequence::SaveAs); + + _saveAllAction = menu->addAction(tr("Save All Flowcharts")); + _saveAllAction->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S)); + _loadAction = menu->addAction(tr("Load Flowchart")); _loadAction->setShortcut(QKeySequence::Open); @@ -145,7 +234,9 @@ FlowchartManager::FlowchartManager(std::shared_ptrsave()) - updateTabText(page); + bool const saved = saveFlowchart(page, /*forceSaveAs*/ false, /*showErrorOnFailure*/ true); + if (saved) + updateWindowModifiedFlag(); } + + updateActionsState(); } -void FlowchartManager::loadCurrentFlowchart() +void FlowchartManager::saveCurrentFlowchartAs() { if (auto *page = currentPage()) { - if (page->load()) - updateTabText(page); + bool const saved = saveFlowchart(page, /*forceSaveAs*/ true, /*showErrorOnFailure*/ false); + if (saved) + updateWindowModifiedFlag(); } + + updateActionsState(); +} + +void FlowchartManager::saveAllFlowcharts() +{ + if (_tabWidget->count() == 0) + return; + + if (_projectDirectory.isEmpty() && !promptForProjectDirectory()) { + updateActionsState(); + return; + } + + if (!ensureProjectLocation()) { + QMessageBox::warning(this, + tr("Save Flowchart"), + tr("Unable to access the selected project folder.")); + updateActionsState(); + return; + } + + QSet usedNames = collectProjectFileNames(); + + bool anySaved = false; + bool encounteredError = false; + + for (int i = 0; i < _tabWidget->count(); ++i) { + FlowchartPage *page = pageAt(i); + if (!page) + continue; + + QString const path = ensureProjectPathFor(page, &usedNames); + if (path.isEmpty()) { + encounteredError = true; + break; + } + + QFileInfo const info(path); + if (!page->isModified() && info.exists()) + continue; + + if (!page->save()) { + encounteredError = true; + break; + } + + anySaved = true; + } + + if (encounteredError) { + QMessageBox::warning(this, + tr("Save Flowchart"), + tr("Failed to save one or more flowcharts.")); + } + + if (anySaved) + updateWindowModifiedFlag(); + + writeProjectManifest(); + updateActionsState(); +} + +void FlowchartManager::loadFlowcharts() +{ + FlowchartPage *page = currentPage(); + + QString directory = !_projectDirectory.isEmpty() ? _projectDirectory : QDir::homePath(); + if (page && !page->filePath().isEmpty()) + directory = QFileInfo(page->filePath()).absolutePath(); + + QFileDialog dialog(this, tr("Open Flowchart"), directory); + dialog.setFileMode(QFileDialog::ExistingFiles); + dialog.setNameFilters({tr("Flowchart Projects (*.flowproj)"), tr("Flow Scene Files (*.flow)")}); + + if (dialog.exec() != QDialog::Accepted) + return; + + QStringList const fileNames = dialog.selectedFiles(); + + if (fileNames.isEmpty()) + return; + + if (fileNames.size() == 1 + && fileNames.first().endsWith(QStringLiteral(".flowproj"), Qt::CaseInsensitive)) { + if (loadProject(fileNames.first())) { + updateWindowModifiedFlag(); + updateActionsState(); + } + return; + } + + QList reusablePages; + for (int i = 0; i < _tabWidget->count(); ++i) { + if (auto *candidate = pageAt(i); candidate && !candidate->isModified() && !candidate->hasContent() + && candidate->filePath().isEmpty()) { + reusablePages.append(candidate); + } + } + + bool anyLoaded = false; + + for (QString const &fileName : fileNames) { + FlowchartPage *targetPage = nullptr; + bool reusedExisting = false; + + if (!reusablePages.isEmpty()) { + targetPage = reusablePages.takeFirst(); + reusedExisting = true; + _tabWidget->setCurrentWidget(targetPage); + } else { + targetPage = createFlowchart(); + } + + int const pageIndex = _tabWidget->indexOf(targetPage); + + if (!targetPage->loadFromFile(fileName)) { + if (reusedExisting) { + reusablePages.prepend(targetPage); + } else if (pageIndex >= 0) { + _tabWidget->removeTab(pageIndex); + targetPage->deleteLater(); + } + + QMessageBox::warning(this, + tr("Open Flowchart"), + tr("Failed to open \"%1\".").arg(QFileInfo(fileName).fileName())); + continue; + } + + updateTabText(targetPage); + _tabWidget->setCurrentWidget(targetPage); + anyLoaded = true; + } + + if (anyLoaded) { + updateWindowModifiedFlag(); + writeProjectManifest(); + } + + updateActionsState(); } void FlowchartManager::closeCurrentFlowchart() @@ -215,6 +451,7 @@ void FlowchartManager::closeFlowchartAt(int index) createFlowchart(); updateWindowModifiedFlag(); + writeProjectManifest(); updateActionsState(); } @@ -264,6 +501,313 @@ FlowchartPage *FlowchartManager::createFlowchart(QString const &displayName) return page; } +bool FlowchartManager::saveFlowchart(FlowchartPage *page, bool forceSaveAs, bool showErrorOnFailure) +{ + if (!page) + return false; + + if (!forceSaveAs && page->filePath().isEmpty()) { + if (_projectDirectory.isEmpty() && !promptForProjectDirectory()) { + forceSaveAs = true; + } else if (!_projectDirectory.isEmpty()) { + if (!ensureProjectLocation()) { + QMessageBox::warning(this, + tr("Save Flowchart"), + tr("Unable to access the selected project folder.")); + return false; + } + + QSet usedNames = collectProjectFileNames(page); + QString const path = ensureProjectPathFor(page, &usedNames); + if (path.isEmpty()) + return false; + } + } + + bool const wasSaved = forceSaveAs ? page->saveAs() : page->save(); + + if (!wasSaved) { + if (showErrorOnFailure) { + QString targetName = page->filePath().isEmpty() ? page->displayName() + : QFileInfo(page->filePath()).fileName(); + + QMessageBox::warning(this, + tr("Save Flowchart"), + tr("Failed to save \"%1\".").arg(targetName)); + } + + return false; + } + + updateTabText(page); + + if (_projectDirectory.isEmpty() && !page->filePath().isEmpty()) { + _projectDirectory = QFileInfo(page->filePath()).absolutePath(); + } + + if (_projectFilePath.isEmpty() && !_projectDirectory.isEmpty()) + _projectFilePath = QDir(_projectDirectory).filePath(QStringLiteral("flowcharts.flowproj")); + + writeProjectManifest(); + + return true; +} + +bool FlowchartManager::promptForProjectDirectory() +{ + QString startDir = _projectDirectory.isEmpty() ? QDir::homePath() : _projectDirectory; + QString const directory = QFileDialog::getExistingDirectory(this, + tr("Select Flowchart Folder"), + startDir, + QFileDialog::ShowDirsOnly); + + if (directory.isEmpty()) + return false; + + _projectDirectory = directory; + + if (_projectFilePath.isEmpty()) + _projectFilePath = QDir(_projectDirectory).filePath(QStringLiteral("flowcharts.flowproj")); + + return ensureProjectLocation(); +} + +bool FlowchartManager::ensureProjectLocation() const +{ + if (_projectDirectory.isEmpty()) + return false; + + QDir dir(_projectDirectory); + if (dir.exists()) + return true; + + return dir.mkpath(QStringLiteral(".")); +} + +QString FlowchartManager::ensureProjectPathFor(FlowchartPage *page, QSet *usedNames) +{ + if (!page || _projectDirectory.isEmpty()) + return QString(); + + QDir projectDir(_projectDirectory); + QString const currentPath = page->filePath(); + + if (!currentPath.isEmpty()) { + QFileInfo const info(currentPath); + if (info.absolutePath() == projectDir.absolutePath()) { + if (usedNames) + usedNames->insert(info.fileName().toLower()); + return currentPath; + } + } + + QSet localUsed = usedNames ? *usedNames : collectProjectFileNames(page); + QString const candidate = nextAvailableFileName(page->displayName(), localUsed); + QString const absolutePath = projectDir.filePath(candidate); + page->setFilePath(absolutePath, false); + + if (usedNames) + usedNames->insert(candidate.toLower()); + + return absolutePath; +} + +QSet FlowchartManager::collectProjectFileNames(FlowchartPage const *exclude) const +{ + QSet names; + if (_projectDirectory.isEmpty()) + return names; + + QDir dir(_projectDirectory); + + QStringList const existingFiles = dir.entryList(QStringList() << QStringLiteral("*.flow"), QDir::Files); + for (QString const &file : existingFiles) + names.insert(file.toLower()); + + if (!_tabWidget) + return names; + + for (int i = 0; i < _tabWidget->count(); ++i) { + FlowchartPage *page = pageAt(i); + if (!page || page == exclude) + continue; + + QString const filePath = page->filePath(); + if (filePath.isEmpty()) + continue; + + QFileInfo const info(filePath); + if (info.absolutePath() != dir.absolutePath()) + continue; + + names.insert(info.fileName().toLower()); + } + + return names; +} + +QString FlowchartManager::nextAvailableFileName(QString const &displayName, QSet &usedNames) const +{ + QString const stem = normalizedFileStem(displayName); + QString candidate = stem + QStringLiteral(".flow"); + int index = 2; + QDir dir(_projectDirectory); + + while (usedNames.contains(candidate.toLower()) || (!dir.path().isEmpty() && dir.exists(candidate))) { + candidate = QStringLiteral("%1_%2.flow").arg(stem).arg(index++); + } + + usedNames.insert(candidate.toLower()); + return candidate; +} + +QString FlowchartManager::normalizedFileStem(QString base) const +{ + base = base.trimmed(); + if (base.isEmpty()) + base = tr("flowchart"); + + static QRegularExpression invalidChars(QStringLiteral("[^\\p{L}\\p{Nd}_-]+")); + base.replace(invalidChars, QStringLiteral("_")); + + static QRegularExpression leadingTrailing(QStringLiteral("^_+|_+$")); + base.remove(leadingTrailing); + + if (base.isEmpty()) + base = QStringLiteral("flowchart"); + + return base; +} + +void FlowchartManager::writeProjectManifest() +{ + if (_projectDirectory.isEmpty()) + return; + + if (_projectFilePath.isEmpty()) + _projectFilePath = QDir(_projectDirectory).filePath(QStringLiteral("flowcharts.flowproj")); + + if (_projectFilePath.isEmpty() || !ensureProjectLocation()) + return; + + QDir projectDir(_projectDirectory); + QJsonArray flowcharts; + + for (int i = 0; i < _tabWidget->count(); ++i) { + FlowchartPage *page = pageAt(i); + if (!page) + continue; + + QString const path = page->filePath(); + if (path.isEmpty()) + continue; + + QFileInfo const info(path); + if (info.absolutePath() != projectDir.absolutePath()) + continue; + + QJsonObject object; + object.insert(QStringLiteral("title"), page->displayName()); + object.insert(QStringLiteral("file"), info.fileName()); + flowcharts.append(object); + } + + QJsonObject root; + root.insert(QStringLiteral("flowcharts"), flowcharts); + + QSaveFile file(_projectFilePath); + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + file.commit(); +} + +bool FlowchartManager::loadProject(QString const &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, + tr("Open Flowchart"), + tr("Failed to open \"%1\".").arg(QFileInfo(fileName).fileName())); + return false; + } + + QJsonParseError error{}; + QJsonDocument const document = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) { + QMessageBox::warning(this, + tr("Open Flowchart"), + tr("\"%1\" is not a valid flowchart project.").arg(QFileInfo(fileName).fileName())); + return false; + } + + QJsonArray const charts = document.object().value(QStringLiteral("flowcharts")).toArray(); + + QList reusablePages; + for (int i = 0; i < _tabWidget->count(); ++i) { + if (auto *candidate = pageAt(i); candidate && !candidate->isModified() && !candidate->hasContent() + && candidate->filePath().isEmpty()) { + reusablePages.append(candidate); + } + } + + _projectFilePath = fileName; + _projectDirectory = QFileInfo(fileName).absolutePath(); + ensureProjectLocation(); + + bool anyLoaded = false; + + for (QJsonValue const &value : charts) { + if (!value.isObject()) + continue; + + QJsonObject const object = value.toObject(); + QString const title = object.value(QStringLiteral("title")).toString(); + QString const relativeFile = object.value(QStringLiteral("file")).toString(); + if (relativeFile.isEmpty()) + continue; + + QString const absolutePath = QDir(_projectDirectory).absoluteFilePath(relativeFile); + + FlowchartPage *targetPage = nullptr; + bool reusedExisting = false; + + if (!reusablePages.isEmpty()) { + targetPage = reusablePages.takeFirst(); + reusedExisting = true; + _tabWidget->setCurrentWidget(targetPage); + } else { + targetPage = createFlowchart(title); + } + + int const pageIndex = _tabWidget->indexOf(targetPage); + + if (!targetPage->loadFromFile(absolutePath, title)) { + if (reusedExisting) { + reusablePages.prepend(targetPage); + } else if (pageIndex >= 0) { + _tabWidget->removeTab(pageIndex); + targetPage->deleteLater(); + } + + QMessageBox::warning(this, + tr("Open Flowchart"), + tr("Failed to open \"%1\".").arg(QFileInfo(absolutePath).fileName())); + continue; + } + + targetPage->setFilePath(absolutePath, false); + updateTabText(targetPage); + _tabWidget->setCurrentWidget(targetPage); + anyLoaded = true; + } + + if (anyLoaded) + writeProjectManifest(); + + return anyLoaded; +} void FlowchartManager::updateTabText(FlowchartPage *page) { if (!page) @@ -298,6 +842,10 @@ void FlowchartManager::updateActionsState() const bool hasPage = currentPage() != nullptr; _saveAction->setEnabled(hasPage); + if (_saveAsAction) + _saveAsAction->setEnabled(hasPage); + if (_saveAllAction) + _saveAllAction->setEnabled(_tabWidget->count() > 0); _loadAction->setEnabled(hasPage); const bool canClose = _tabWidget->count() > 1; diff --git a/examples/calculator/FlowchartManager.hpp b/examples/calculator/FlowchartManager.hpp index fdf8a3bb7..ac0c5f419 100644 --- a/examples/calculator/FlowchartManager.hpp +++ b/examples/calculator/FlowchartManager.hpp @@ -9,6 +9,7 @@ #include #include +#include #include class QAction; @@ -24,13 +25,18 @@ class FlowchartPage : public QWidget QWidget *parent = nullptr); bool save(); + bool saveAs(); bool load(); + bool loadFromFile(QString const &fileName, QString const &displayNameOverride = QString()); bool isModified() const { return _isModified; } QString displayName() const { return _displayName; } + QString filePath() const { return _filePath; } + bool hasContent() const; void setDisplayName(QString name); + void setFilePath(QString filePath, bool updateDisplayName = true); public Q_SLOTS: void runFlowchart(); @@ -40,6 +46,7 @@ public Q_SLOTS: private: void setModified(bool modified); + bool saveToPath(QString const &fileName, bool updateDisplayName); private: QtNodes::DataFlowGraphModel _model; @@ -48,6 +55,7 @@ public Q_SLOTS: QPushButton *_runButton = nullptr; bool _isModified = false; QString _displayName; + QString _filePath; }; class FlowchartManager : public QWidget @@ -61,7 +69,9 @@ class FlowchartManager : public QWidget private Q_SLOTS: void addFlowchart(); void saveCurrentFlowchart(); - void loadCurrentFlowchart(); + void saveCurrentFlowchartAs(); + void saveAllFlowcharts(); + void loadFlowcharts(); void closeCurrentFlowchart(); void closeFlowchartAt(int index); void runCurrentFlowchart(); @@ -72,6 +82,18 @@ private Q_SLOTS: FlowchartPage *pageAt(int index) const; FlowchartPage *createFlowchart(QString const &displayName = QString()); + bool saveFlowchart(FlowchartPage *page, + bool forceSaveAs = false, + bool showErrorOnFailure = true); + + bool promptForProjectDirectory(); + bool ensureProjectLocation() const; + QString ensureProjectPathFor(FlowchartPage *page, QSet *usedNames = nullptr); + QSet collectProjectFileNames(FlowchartPage const *exclude = nullptr) const; + QString nextAvailableFileName(QString const &displayName, QSet &usedNames) const; + QString normalizedFileStem(QString base) const; + void writeProjectManifest(); + bool loadProject(QString const &fileName); void updateTabText(FlowchartPage *page); void updateWindowModifiedFlag(); @@ -83,9 +105,13 @@ private Q_SLOTS: QTabWidget *_tabWidget = nullptr; QAction *_newAction = nullptr; QAction *_saveAction = nullptr; + QAction *_saveAsAction = nullptr; + QAction *_saveAllAction = nullptr; QAction *_loadAction = nullptr; QAction *_closeAction = nullptr; QPushButton *_runCurrentButton = nullptr; QPushButton *_runAllButton = nullptr; int _flowchartCount = 0; + QString _projectFilePath; + QString _projectDirectory; }; diff --git a/include/QtNodes/internal/DataFlowGraphicsScene.hpp b/include/QtNodes/internal/DataFlowGraphicsScene.hpp index e9f89cac0..78fb81b47 100644 --- a/include/QtNodes/internal/DataFlowGraphicsScene.hpp +++ b/include/QtNodes/internal/DataFlowGraphicsScene.hpp @@ -27,6 +27,10 @@ public Q_SLOTS: bool save() const; bool load(); +public: + bool saveToFile(QString const &fileName) const; + bool loadFromFile(QString const &fileName); + Q_SIGNALS: void sceneLoaded(); diff --git a/src/DataFlowGraphicsScene.cpp b/src/DataFlowGraphicsScene.cpp index 32970608e..ab177ac67 100644 --- a/src/DataFlowGraphicsScene.cpp +++ b/src/DataFlowGraphicsScene.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -148,21 +149,17 @@ QMenu *DataFlowGraphicsScene::createSceneMenu(QPointF const scenePos) bool DataFlowGraphicsScene::save() const { QString fileName = QFileDialog::getSaveFileName(nullptr, - tr("Open Flow Scene"), + tr("Save Flow Scene"), QDir::homePath(), tr("Flow Scene Files (*.flow)")); - if (!fileName.isEmpty()) { - if (!fileName.endsWith("flow", Qt::CaseInsensitive)) - fileName += ".flow"; + if (fileName.isEmpty()) + return false; - QFile file(fileName); - if (file.open(QIODevice::WriteOnly)) { - file.write(QJsonDocument(_graphModel.save()).toJson()); - return true; - } - } - return false; + if (!fileName.endsWith(".flow", Qt::CaseInsensitive)) + fileName += ".flow"; + + return saveToFile(fileName); } bool DataFlowGraphicsScene::load() @@ -172,6 +169,27 @@ bool DataFlowGraphicsScene::load() QDir::homePath(), tr("Flow Scene Files (*.flow)")); + if (fileName.isEmpty()) + return false; + + return loadFromFile(fileName); +} + +bool DataFlowGraphicsScene::saveToFile(QString const &fileName) const +{ + if (fileName.isEmpty()) + return false; + + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) + return false; + + file.write(QJsonDocument(_graphModel.save()).toJson()); + return true; +} + +bool DataFlowGraphicsScene::loadFromFile(QString const &fileName) +{ if (!QFileInfo::exists(fileName)) return false;