diff --git a/src/application/LayoutReviewWidget.cpp b/src/application/LayoutReviewWidget.cpp index 62d36e4..c508932 100644 --- a/src/application/LayoutReviewWidget.cpp +++ b/src/application/LayoutReviewWidget.cpp @@ -58,6 +58,7 @@ bool isLiveValidationIssue(safecrowd::domain::ImportIssueCode code) { switch (code) { case ImportIssueCode::MissingExit: + case ImportIssueCode::MissingRoom: case ImportIssueCode::DisconnectedWalkableArea: case ImportIssueCode::WidthBelowMinimum: return true; @@ -381,6 +382,7 @@ LayoutReviewWidget::LayoutReviewWidget( : QWidget(parent), projectName_(projectName), importResult_(importResult), + openProjectHandler_(std::move(openProjectHandler)), approvalHandler_(std::move(approvalHandler)) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -404,7 +406,8 @@ LayoutReviewWidget::LayoutReviewWidget( shell_->setTools({"Project", "Tool"}); shell_->setSaveProjectHandler(std::move(saveProjectHandler)); - shell_->setOpenProjectHandler(std::move(openProjectHandler)); + shell_->setOpenProjectHandler(openProjectHandler_); + shell_->setBackHandler(openProjectHandler_); shell_->setCanvas(preview_); shell_->setReviewPanel(reviewPanel); diff --git a/src/application/LayoutReviewWidget.h b/src/application/LayoutReviewWidget.h index 865cc64..3533404 100644 --- a/src/application/LayoutReviewWidget.h +++ b/src/application/LayoutReviewWidget.h @@ -50,6 +50,7 @@ class LayoutReviewWidget : public QWidget { QString projectName_{}; safecrowd::domain::ImportResult importResult_{}; + std::function openProjectHandler_{}; std::function approvalHandler_{}; std::vector undoHistory_{}; WorkspaceShell* shell_{nullptr}; diff --git a/src/application/MainWindow.cpp b/src/application/MainWindow.cpp index 0d91c19..06c615a 100644 --- a/src/application/MainWindow.cpp +++ b/src/application/MainWindow.cpp @@ -161,6 +161,7 @@ void MainWindow::saveCurrentProject() { void MainWindow::showLayoutReview(const ProjectMetadata& metadata) { currentProject_ = metadata; hasCurrentProject_ = true; + lastApprovedImportResult_.reset(); auto importResult = metadata.isBuiltInDemo() ? makeDemoImportResult() @@ -177,6 +178,13 @@ void MainWindow::showLayoutReview(const ProjectMetadata& metadata) { applySavedReviewState(metadata, &importResult); + showLayoutReview(metadata, std::move(importResult)); +} + +void MainWindow::showLayoutReview(const ProjectMetadata& metadata, safecrowd::domain::ImportResult importResult) { + currentProject_ = metadata; + hasCurrentProject_ = true; + setCentralWidget(new LayoutReviewWidget( metadata.name, importResult, @@ -189,6 +197,7 @@ void MainWindow::showLayoutReview(const ProjectMetadata& metadata) { showProjectNavigator(); }, [this](const safecrowd::domain::ImportResult& approvedImportResult) { + lastApprovedImportResult_ = approvedImportResult; showScenarioAuthoring(approvedImportResult); }, this)); @@ -200,6 +209,8 @@ void MainWindow::showScenarioAuthoring(const safecrowd::domain::ImportResult& im return; } + lastApprovedImportResult_ = importResult; + setCentralWidget(new ScenarioAuthoringWidget( currentProject_.name, *importResult.layout, @@ -211,6 +222,13 @@ void MainWindow::showScenarioAuthoring(const safecrowd::domain::ImportResult& im currentProject_ = {}; showProjectNavigator(); }, + [this]() { + if (lastApprovedImportResult_.has_value()) { + showLayoutReview(currentProject_, *lastApprovedImportResult_); + } else { + showLayoutReview(currentProject_); + } + }, this)); } diff --git a/src/application/MainWindow.h b/src/application/MainWindow.h index aa235ff..f35d16b 100644 --- a/src/application/MainWindow.h +++ b/src/application/MainWindow.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include "application/ProjectMetadata.h" @@ -26,11 +28,13 @@ class MainWindow : public QMainWindow { void openProject(const ProjectMetadata& metadata); void saveCurrentProject(); void showLayoutReview(const ProjectMetadata& metadata); + void showLayoutReview(const ProjectMetadata& metadata, safecrowd::domain::ImportResult importResult); void showScenarioAuthoring(const safecrowd::domain::ImportResult& importResult); safecrowd::domain::SafeCrowdDomain& domain_; ProjectMetadata currentProject_{}; bool hasCurrentProject_{false}; + std::optional lastApprovedImportResult_{}; }; } // namespace safecrowd::application diff --git a/src/application/ScenarioAuthoringWidget.cpp b/src/application/ScenarioAuthoringWidget.cpp index 2cd99d9..b4e11f6 100644 --- a/src/application/ScenarioAuthoringWidget.cpp +++ b/src/application/ScenarioAuthoringWidget.cpp @@ -246,12 +246,14 @@ ScenarioAuthoringWidget::ScenarioAuthoringWidget( const safecrowd::domain::FacilityLayout2D& layout, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent) : QWidget(parent), projectName_(projectName), layout_(layout), saveProjectHandler_(std::move(saveProjectHandler)), - openProjectHandler_(std::move(openProjectHandler)) { + openProjectHandler_(std::move(openProjectHandler)), + backToLayoutReviewHandler_(std::move(backToLayoutReviewHandler)) { initializeUi(true); } @@ -261,12 +263,14 @@ ScenarioAuthoringWidget::ScenarioAuthoringWidget( InitialState initialState, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent) : QWidget(parent), projectName_(projectName), layout_(layout), saveProjectHandler_(std::move(saveProjectHandler)), openProjectHandler_(std::move(openProjectHandler)), + backToLayoutReviewHandler_(std::move(backToLayoutReviewHandler)), scenarios_(std::move(initialState.scenarios)), currentScenarioIndex_(initialState.currentScenarioIndex), navigationView_(initialState.navigationView), @@ -283,6 +287,7 @@ void ScenarioAuthoringWidget::initializeUi(bool promptForScenario) { shell_->setTools({"Project"}); shell_->setSaveProjectHandler(saveProjectHandler_); shell_->setOpenProjectHandler(openProjectHandler_); + shell_->setBackHandler(backToLayoutReviewHandler_); shell_->setTopBarTrailingWidget(createTopBarTogglePanel()); refreshRightPanel(); rootLayout->addWidget(shell_); @@ -541,6 +546,7 @@ void ScenarioAuthoringWidget::runFirstStagedBaselineScenario() { scenario->draft, saveProjectHandler_, openProjectHandler_, + backToLayoutReviewHandler_, this); rootLayout->replaceWidget(shell_, runWidget); shell_->hide(); diff --git a/src/application/ScenarioAuthoringWidget.h b/src/application/ScenarioAuthoringWidget.h index 4f8387b..f23a473 100644 --- a/src/application/ScenarioAuthoringWidget.h +++ b/src/application/ScenarioAuthoringWidget.h @@ -25,6 +25,7 @@ class ScenarioAuthoringWidget : public QWidget { const safecrowd::domain::FacilityLayout2D& layout, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent = nullptr); enum class NavigationView { @@ -62,6 +63,7 @@ class ScenarioAuthoringWidget : public QWidget { InitialState initialState, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent = nullptr); private: @@ -92,6 +94,7 @@ class ScenarioAuthoringWidget : public QWidget { safecrowd::domain::FacilityLayout2D layout_{}; std::function saveProjectHandler_{}; std::function openProjectHandler_{}; + std::function backToLayoutReviewHandler_{}; std::vector scenarios_{}; int currentScenarioIndex_{-1}; NavigationView navigationView_{NavigationView::Layout}; diff --git a/src/application/ScenarioResultWidget.cpp b/src/application/ScenarioResultWidget.cpp index 29e71b4..afe2b8d 100644 --- a/src/application/ScenarioResultWidget.cpp +++ b/src/application/ScenarioResultWidget.cpp @@ -336,6 +336,7 @@ ScenarioResultWidget::ScenarioResultWidget( safecrowd::domain::ScenarioRiskSnapshot risk, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent) : QWidget(parent), projectName_(std::move(projectName)), @@ -344,7 +345,8 @@ ScenarioResultWidget::ScenarioResultWidget( frame_(std::move(frame)), risk_(std::move(risk)), saveProjectHandler_(std::move(saveProjectHandler)), - openProjectHandler_(std::move(openProjectHandler)) { + openProjectHandler_(std::move(openProjectHandler)), + backToLayoutReviewHandler_(std::move(backToLayoutReviewHandler)) { auto* rootLayout = new QVBoxLayout(this); rootLayout->setContentsMargins(0, 0, 0, 0); rootLayout->setSpacing(0); @@ -358,6 +360,9 @@ ScenarioResultWidget::ScenarioResultWidget( shell_->setTools({"Project"}); shell_->setSaveProjectHandler(saveProjectHandler_); shell_->setOpenProjectHandler(openProjectHandler_); + shell_->setBackHandler([this]() { + navigateToAuthoring(true); + }); auto* canvas = new SimulationCanvasWidget(layout_, shell_); canvas->setFrame(frame_); @@ -406,6 +411,7 @@ void ScenarioResultWidget::rerunScenario() { scenario_, saveProjectHandler_, openProjectHandler_, + backToLayoutReviewHandler_, this); rootLayout->replaceWidget(shell_, runWidget); @@ -434,6 +440,7 @@ void ScenarioResultWidget::navigateToAuthoring(bool showRunPanel) { std::move(initial), saveProjectHandler_, openProjectHandler_, + backToLayoutReviewHandler_, this); rootLayout->replaceWidget(shell_, authoringWidget); diff --git a/src/application/ScenarioResultWidget.h b/src/application/ScenarioResultWidget.h index f23fcf2..983911a 100644 --- a/src/application/ScenarioResultWidget.h +++ b/src/application/ScenarioResultWidget.h @@ -24,6 +24,7 @@ class ScenarioResultWidget : public QWidget { safecrowd::domain::ScenarioRiskSnapshot risk, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent = nullptr); private: @@ -37,6 +38,7 @@ class ScenarioResultWidget : public QWidget { safecrowd::domain::ScenarioRiskSnapshot risk_{}; std::function saveProjectHandler_{}; std::function openProjectHandler_{}; + std::function backToLayoutReviewHandler_{}; WorkspaceShell* shell_{nullptr}; }; diff --git a/src/application/ScenarioRunWidget.cpp b/src/application/ScenarioRunWidget.cpp index e495786..2dd7efa 100644 --- a/src/application/ScenarioRunWidget.cpp +++ b/src/application/ScenarioRunWidget.cpp @@ -168,6 +168,7 @@ ScenarioRunWidget::ScenarioRunWidget( const safecrowd::domain::ScenarioDraft& scenario, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent) : QWidget(parent), projectName_(projectName), @@ -175,7 +176,8 @@ ScenarioRunWidget::ScenarioRunWidget( scenario_(scenario), runner_(layout_, scenario_), saveProjectHandler_(std::move(saveProjectHandler)), - openProjectHandler_(std::move(openProjectHandler)) { + openProjectHandler_(std::move(openProjectHandler)), + backToLayoutReviewHandler_(std::move(backToLayoutReviewHandler)) { auto* rootLayout = new QVBoxLayout(this); rootLayout->setContentsMargins(0, 0, 0, 0); rootLayout->setSpacing(0); @@ -189,13 +191,15 @@ ScenarioRunWidget::ScenarioRunWidget( shell_->setTools({"Project"}); shell_->setSaveProjectHandler(saveProjectHandler_); shell_->setOpenProjectHandler(openProjectHandler_); + shell_->setBackHandler([this]() { + returnToAuthoring(); + }); canvas_ = new SimulationCanvasWidget(layout_, shell_); canvas_->setFrame(runner_.frame()); shell_->setCanvas(canvas_); shell_->setReviewPanel(createRunPanel()); shell_->setReviewPanelVisible(true); rootLayout->addWidget(shell_); - addBackToAuthoringButton(); timer_ = new QTimer(this); timer_->setInterval(33); @@ -282,36 +286,6 @@ QWidget* ScenarioRunWidget::createRunPanel() { return panel; } -void ScenarioRunWidget::addBackToAuthoringButton() { - if (canvas_ == nullptr) { - return; - } - - auto* button = new QPushButton("<", canvas_); - button->setToolTip("Back to scenario editor"); - button->setAccessibleName("Back to scenario editor"); - button->setFixedSize(40, 36); - button->move(16, 16); - button->raise(); - button->setStyleSheet( - "QPushButton {" - " background: rgba(255, 255, 255, 232);" - " border: 1px solid #c9d5e2;" - " border-radius: 10px;" - " color: #16202b;" - " font-size: 18px;" - " font-weight: 700;" - " padding-bottom: 2px;" - "}" - "QPushButton:hover {" - " background: #eef3f8;" - " border-color: #b8c6d6;" - "}"); - connect(button, &QPushButton::clicked, this, [this]() { - returnToAuthoring(); - }); -} - void ScenarioRunWidget::returnToAuthoring() { if (timer_ != nullptr) { timer_->stop(); @@ -334,6 +308,7 @@ void ScenarioRunWidget::returnToAuthoring() { std::move(initial), saveProjectHandler_, openProjectHandler_, + backToLayoutReviewHandler_, this); rootLayout->replaceWidget(shell_, authoringWidget); @@ -450,6 +425,7 @@ void ScenarioRunWidget::showResults() { openProjectHandler_(); } }, + backToLayoutReviewHandler_, this); rootLayout->replaceWidget(shell_, resultWidget); shell_->hide(); diff --git a/src/application/ScenarioRunWidget.h b/src/application/ScenarioRunWidget.h index 2e86f7f..81663b8 100644 --- a/src/application/ScenarioRunWidget.h +++ b/src/application/ScenarioRunWidget.h @@ -27,11 +27,11 @@ class ScenarioRunWidget : public QWidget { const safecrowd::domain::ScenarioDraft& scenario, std::function saveProjectHandler, std::function openProjectHandler, + std::function backToLayoutReviewHandler, QWidget* parent = nullptr); private: QWidget* createRunPanel(); - void addBackToAuthoringButton(); void returnToAuthoring(); void refreshStatus(); void showResults(); @@ -44,6 +44,7 @@ class ScenarioRunWidget : public QWidget { safecrowd::domain::ScenarioSimulationRunner runner_{}; std::function saveProjectHandler_{}; std::function openProjectHandler_{}; + std::function backToLayoutReviewHandler_{}; WorkspaceShell* shell_{nullptr}; SimulationCanvasWidget* canvas_{nullptr}; QTimer* timer_{nullptr}; diff --git a/src/application/WorkspaceShell.cpp b/src/application/WorkspaceShell.cpp index 9047745..3f32e1f 100644 --- a/src/application/WorkspaceShell.cpp +++ b/src/application/WorkspaceShell.cpp @@ -57,6 +57,26 @@ QPushButton* createFlatTopBarButton(QWidget* parent, const QString& text) { return button; } +QPushButton* createFlatTopBarIconButton(QWidget* parent, const QString& text) { + auto* button = new QPushButton(text, parent); + button->setFont(ui::font(ui::FontRole::Body)); + button->setFixedSize(32, 32); + button->setCursor(Qt::PointingHandCursor); + button->setStyleSheet( + "QPushButton {" + " background: transparent;" + " border: 0;" + " border-radius: 10px;" + " color: #16202b;" + " font-size: 18px;" + " font-weight: 700;" + "}" + "QPushButton:hover {" + " background: #eef3f8;" + "}"); + return button; +} + } // namespace WorkspaceShell::WorkspaceShell(QWidget* parent) @@ -173,9 +193,42 @@ void WorkspaceShell::setFixedWidthVisible(QWidget* widget, bool visible, int wid } void WorkspaceShell::setTools(const QStringList& tools) { + tools_ = tools; + rebuildTopBar(); +} + +void WorkspaceShell::setBackHandler(std::function handler) { + backHandler_ = std::move(handler); + rebuildTopBar(); +} + +void WorkspaceShell::clearTopBar() { + while (auto* item = topBarLayout_->takeAt(0)) { + delete item->widget(); + delete item; + } + + openProjectAction_ = nullptr; + saveProjectAction_ = nullptr; + backButton_ = nullptr; +} + +void WorkspaceShell::rebuildTopBar() { clearTopBar(); - for (const auto& tool : tools) { + if (backHandler_) { + backButton_ = createFlatTopBarIconButton(this, "<"); + backButton_->setToolTip("Back"); + backButton_->setAccessibleName("Back"); + connect(backButton_, &QPushButton::clicked, this, [this]() { + if (backHandler_) { + backHandler_(); + } + }); + topBarLayout_->addWidget(backButton_); + } + + for (const auto& tool : tools_) { auto* button = createTopBarButton(tool); if (tool == "Project") { auto* menu = new QMenu(button); @@ -195,17 +248,6 @@ void WorkspaceShell::setTools(const QStringList& tools) { } topBarLayout_->addWidget(button); } - -} - -void WorkspaceShell::clearTopBar() { - while (auto* item = topBarLayout_->takeAt(0)) { - delete item->widget(); - delete item; - } - - openProjectAction_ = nullptr; - saveProjectAction_ = nullptr; } QPushButton* WorkspaceShell::createTopBarButton(const QString& text) { diff --git a/src/application/WorkspaceShell.h b/src/application/WorkspaceShell.h index 8dc0a0b..0284c90 100644 --- a/src/application/WorkspaceShell.h +++ b/src/application/WorkspaceShell.h @@ -35,6 +35,7 @@ class WorkspaceShell : public QWidget { explicit WorkspaceShell(WorkspaceShellOptions options, QWidget* parent = nullptr); void setTools(const QStringList& tools); + void setBackHandler(std::function handler); void setNavigationRail(QWidget* rail); void setNavigationPanel(QWidget* panel); void setNavigationVisible(bool visible); @@ -50,6 +51,7 @@ class WorkspaceShell : public QWidget { void initialize(const WorkspaceShellOptions& options); void setFixedWidthVisible(QWidget* widget, bool visible, int width); void clearTopBar(); + void rebuildTopBar(); QPushButton* createTopBarButton(const QString& text); QFrame* topBar_{nullptr}; @@ -70,6 +72,9 @@ class WorkspaceShell : public QWidget { QAction* saveProjectAction_{nullptr}; std::function openProjectHandler_{}; std::function saveProjectHandler_{}; + std::function backHandler_{}; + QStringList tools_{}; + QPushButton* backButton_{nullptr}; }; } // namespace safecrowd::application diff --git a/src/domain/ImportIssue.cpp b/src/domain/ImportIssue.cpp index 7454a93..d54c49e 100644 --- a/src/domain/ImportIssue.cpp +++ b/src/domain/ImportIssue.cpp @@ -31,6 +31,8 @@ const char* toString(ImportIssueCode code) noexcept { return "UnsupportedEntity"; case ImportIssueCode::MissingSourceGeometry: return "MissingSourceGeometry"; + case ImportIssueCode::MissingRoom: + return "MissingRoom"; case ImportIssueCode::MissingBlockDefinition: return "MissingBlockDefinition"; case ImportIssueCode::InvalidGeometry: diff --git a/src/domain/ImportIssue.h b/src/domain/ImportIssue.h index 14f530d..db03cd0 100644 --- a/src/domain/ImportIssue.h +++ b/src/domain/ImportIssue.h @@ -17,6 +17,7 @@ enum class ImportIssueCode { FileReadFailed, UnsupportedEntity, MissingSourceGeometry, + MissingRoom, MissingBlockDefinition, InvalidGeometry, DisconnectedWalkableArea, diff --git a/src/domain/ImportValidationService.cpp b/src/domain/ImportValidationService.cpp index b56693a..13ad7d2 100644 --- a/src/domain/ImportValidationService.cpp +++ b/src/domain/ImportValidationService.cpp @@ -70,10 +70,14 @@ std::vector ImportValidationService::validate(const FacilityLayout2 std::vector issues; std::unordered_set exitZoneIds; + std::size_t roomZoneCount = 0; for (const auto& zone : layout.zones) { if (zone.kind == ZoneKind::Exit) { exitZoneIds.insert(zone.id); } + if (zone.kind == ZoneKind::Room) { + ++roomZoneCount; + } } if (exitZoneIds.empty()) { @@ -86,6 +90,15 @@ std::vector ImportValidationService::validate(const FacilityLayout2 }); } + if (roomZoneCount == 0) { + issues.push_back({ + .severity = ImportIssueSeverity::Warning, + .code = ImportIssueCode::MissingRoom, + .message = "Agents can only be placed inside Room or Exit zones.", + .targetId = layout.id, + }); + } + for (const auto& connection : layout.connections) { if (connection.effectiveWidth > 0.0 && connection.effectiveWidth < kMinimumConnectionWidth) { issues.push_back({