diff --git a/src/application/LayoutNavigationPanelWidget.cpp b/src/application/LayoutNavigationPanelWidget.cpp index 46fedd7..924ad8e 100644 --- a/src/application/LayoutNavigationPanelWidget.cpp +++ b/src/application/LayoutNavigationPanelWidget.cpp @@ -374,7 +374,8 @@ LayoutNavigationPanelWidget::LayoutNavigationPanelWidget( QWidget* parent, QWidget* headerWidget, NavigationTreeState navigationState, - std::function&)> expandedStateChangedHandler) + std::function&)> expandedStateChangedHandler, + std::function deleteElementHandler) : QWidget(parent) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -387,7 +388,10 @@ LayoutNavigationPanelWidget::LayoutNavigationPanelWidget( this, headerWidget, std::move(navigationState), - std::move(expandedStateChangedHandler))); + std::move(expandedStateChangedHandler), + std::move(deleteElementHandler), + {}, + QString("Settings..."))); } } // namespace safecrowd::application diff --git a/src/application/LayoutNavigationPanelWidget.h b/src/application/LayoutNavigationPanelWidget.h index 21c82c6..0334ba4 100644 --- a/src/application/LayoutNavigationPanelWidget.h +++ b/src/application/LayoutNavigationPanelWidget.h @@ -19,7 +19,8 @@ class LayoutNavigationPanelWidget : public QWidget { QWidget* parent = nullptr, QWidget* headerWidget = nullptr, NavigationTreeState navigationState = {}, - std::function&)> expandedStateChangedHandler = {}); + std::function&)> expandedStateChangedHandler = {}, + std::function deleteElementHandler = {}); }; } // namespace safecrowd::application diff --git a/src/application/LayoutPreviewWidget.cpp b/src/application/LayoutPreviewWidget.cpp index 4aef6e2..e541c76 100644 --- a/src/application/LayoutPreviewWidget.cpp +++ b/src/application/LayoutPreviewWidget.cpp @@ -2992,6 +2992,33 @@ void LayoutPreviewWidget::focusElement(const QString& elementId) { focusIssueTarget(elementId); } +bool LayoutPreviewWidget::deleteElement(const QString& elementId) { + if (!importResult_.layout.has_value() || elementId.isEmpty() || elementId.startsWith("floor:")) { + return false; + } + + if (containsConnection(*importResult_.layout, elementId)) { + deleteConnection(elementId); + return true; + } + if (containsBarrier(*importResult_.layout, elementId)) { + deleteBarrier(elementId); + return true; + } + if (containsZone(*importResult_.layout, elementId)) { + selectedBarrierId_.clear(); + selectedBarrierIds_.clear(); + selectedConnectionId_.clear(); + selectedConnectionIds_.clear(); + selectedZoneId_ = elementId; + selectedZoneIds_ = QStringList{elementId}; + deleteSelectedElements(); + return true; + } + + return false; +} + void LayoutPreviewWidget::focusIssueTarget(const QString& targetId) { selectedZoneId_.clear(); selectedZoneIds_.clear(); diff --git a/src/application/LayoutPreviewWidget.h b/src/application/LayoutPreviewWidget.h index d2190f4..89d54e5 100644 --- a/src/application/LayoutPreviewWidget.h +++ b/src/application/LayoutPreviewWidget.h @@ -52,6 +52,7 @@ class LayoutPreviewWidget : public QWidget { void focusElement(const QString& elementId); void focusIssueTarget(const QString& targetId); + bool deleteElement(const QString& elementId); void resetView(); void setImportResult(safecrowd::domain::ImportResult importResult); void setSelectionChangedHandler(std::function handler); diff --git a/src/application/LayoutReviewWidget.cpp b/src/application/LayoutReviewWidget.cpp index a719089..568a2ff 100644 --- a/src/application/LayoutReviewWidget.cpp +++ b/src/application/LayoutReviewWidget.cpp @@ -157,6 +157,7 @@ QWidget* createNavigationPanel( bool showIssues, std::function selectIssueHandler, std::function selectLayoutElementHandler, + std::function deleteLayoutElementHandler, NavigationTreeState layoutNavigationState, std::function&)> layoutExpandedStateChangedHandler, const WorkspaceShell* shell, @@ -173,7 +174,8 @@ QWidget* createNavigationPanel( content, shell != nullptr ? shell->createPanelHeader("Layout", content, false) : nullptr, std::move(layoutNavigationState), - std::move(layoutExpandedStateChangedHandler))); + std::move(layoutExpandedStateChangedHandler), + std::move(deleteLayoutElementHandler))); return content; } @@ -503,6 +505,11 @@ void LayoutReviewWidget::refreshNavigationPanel() { [this](const QString& elementId) { handleLayoutElementSelected(elementId); }, + [this](const QString& elementId) { + if (preview_ != nullptr) { + preview_->deleteElement(elementId); + } + }, NavigationTreeState{ .expandedNodeIds = layoutExpandedNodeIds_, .selectedId = selectedLayoutElementId_, diff --git a/src/application/NavigationTreeWidget.cpp b/src/application/NavigationTreeWidget.cpp index c32534a..9dce76fc3 100644 --- a/src/application/NavigationTreeWidget.cpp +++ b/src/application/NavigationTreeWidget.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -274,7 +275,10 @@ NavigationTreeWidget::NavigationTreeWidget( QWidget* parent, QWidget* headerWidget, NavigationTreeState state, - std::function&)> expandedStateChangedHandler) + std::function&)> expandedStateChangedHandler, + std::function deleteItemHandler, + std::function settingsItemHandler, + const QString& settingsLabel) : QWidget(parent) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -358,6 +362,57 @@ NavigationTreeWidget::NavigationTreeWidget( }); } + if (deleteItemHandler || settingsItemHandler) { + auto* viewport = tree->viewport(); + if (viewport != nullptr) { + tree->setContextMenuPolicy(Qt::CustomContextMenu); + viewport->setContextMenuPolicy(Qt::CustomContextMenu); + + const auto showMenu = [tree, viewport, deleteItemHandler, settingsItemHandler, settingsLabel](const QPoint& pos, QWidget* source) { + auto* item = tree->itemAt(pos); + if (item == nullptr) { + return; + } + + const auto selectable = item->data(0, kSelectableRole).toBool(); + const auto id = item->data(0, kIdRole).toString(); + if (!selectable || id.isEmpty()) { + return; + } + + QMenu menu; + QAction* settingsAction = nullptr; + if (settingsItemHandler) { + settingsAction = menu.addAction(settingsLabel); + } + QAction* deleteAction = nullptr; + if (deleteItemHandler) { + deleteAction = menu.addAction("Delete"); + if (id.startsWith("floor:")) { + deleteAction->setEnabled(false); + } + } + + const auto* selectedAction = menu.exec(source->mapToGlobal(pos)); + if (selectedAction == nullptr) { + return; + } + if (selectedAction == settingsAction && settingsItemHandler) { + settingsItemHandler(id); + } else if (selectedAction == deleteAction && deleteItemHandler) { + deleteItemHandler(id); + } + }; + + QObject::connect(viewport, &QWidget::customContextMenuRequested, tree, [showMenu, viewport](const QPoint& pos) { + showMenu(pos, viewport); + }); + QObject::connect(tree, &QWidget::customContextMenuRequested, tree, [showMenu, tree](const QPoint& pos) { + showMenu(pos, tree); + }); + } + } + layout->addWidget(tree, 1); } diff --git a/src/application/NavigationTreeWidget.h b/src/application/NavigationTreeWidget.h index d75dfff..9879af8 100644 --- a/src/application/NavigationTreeWidget.h +++ b/src/application/NavigationTreeWidget.h @@ -36,7 +36,10 @@ class NavigationTreeWidget : public QWidget { QWidget* parent = nullptr, QWidget* headerWidget = nullptr, NavigationTreeState state = {}, - std::function&)> expandedStateChangedHandler = {}); + std::function&)> expandedStateChangedHandler = {}, + std::function deleteItemHandler = {}, + std::function settingsItemHandler = {}, + const QString& settingsLabel = QString("Settings...")); }; } // namespace safecrowd::application diff --git a/src/application/ScenarioAuthoringWidget.cpp b/src/application/ScenarioAuthoringWidget.cpp index f960603..e421d42 100644 --- a/src/application/ScenarioAuthoringWidget.cpp +++ b/src/application/ScenarioAuthoringWidget.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include #include #include @@ -12,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +40,58 @@ QLabel* createLabel(const QString& text, QWidget* parent, ui::FontRole role = ui return label; } +bool editOperationalEvent( + safecrowd::domain::OperationalEventDraft* event, + QWidget* parent) { + if (event == nullptr) { + return false; + } + + QDialog dialog(parent); + dialog.setWindowTitle("Edit event"); + + auto* root = new QVBoxLayout(&dialog); + root->setContentsMargins(16, 16, 16, 16); + root->setSpacing(12); + + auto* form = new QFormLayout(); + form->setContentsMargins(0, 0, 0, 0); + form->setSpacing(8); + + auto* nameEdit = new QLineEdit(&dialog); + nameEdit->setText(QString::fromStdString(event->name)); + auto* triggerEdit = new QPlainTextEdit(&dialog); + triggerEdit->setPlainText(QString::fromStdString(event->triggerSummary)); + triggerEdit->setMinimumHeight(72); + auto* targetEdit = new QPlainTextEdit(&dialog); + targetEdit->setPlainText(QString::fromStdString(event->targetSummary)); + targetEdit->setMinimumHeight(72); + + form->addRow("Name", nameEdit); + form->addRow("Trigger", triggerEdit); + form->addRow("Target", targetEdit); + root->addLayout(form); + + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + root->addWidget(buttons); + + if (dialog.exec() != QDialog::Accepted) { + return false; + } + + const auto name = nameEdit->text().trimmed(); + if (name.isEmpty()) { + return false; + } + + event->name = name.toStdString(); + event->triggerSummary = triggerEdit->toPlainText().trimmed().toStdString(); + event->targetSummary = targetEdit->toPlainText().trimmed().toStdString(); + return true; +} + QString zoneLabel(const safecrowd::domain::Zone2D& zone) { const auto id = QString::fromStdString(zone.id); const auto label = QString::fromStdString(zone.label); @@ -459,6 +515,7 @@ QWidget* createCrowdPanel( std::function selectPlacementHandler, NavigationTreeState navigationState, std::function&)> expandedStateChangedHandler, + std::function deletePlacementHandler, const WorkspaceShell* shell, QWidget* parent) { return new NavigationTreeWidget( @@ -469,7 +526,8 @@ QWidget* createCrowdPanel( parent, shell != nullptr ? shell->createPanelHeader("Crowd", parent, false) : nullptr, std::move(navigationState), - std::move(expandedStateChangedHandler)); + std::move(expandedStateChangedHandler), + std::move(deletePlacementHandler)); } std::vector buildEventsTree( @@ -613,14 +671,21 @@ QWidget* createEventsPanel( const safecrowd::domain::FacilityLayout2D& layout, const ScenarioAuthoringWidget::ScenarioState* scenario, const WorkspaceShell* shell, - QWidget* parent) { + QWidget* parent, + std::function deleteItemHandler, + std::function settingsItemHandler) { return new NavigationTreeWidget( "Events", buildEventsTree(layout, scenario), "No operational events or blocked exits yet", {}, parent, - shell != nullptr ? shell->createPanelHeader("Events", parent, false) : nullptr); + shell != nullptr ? shell->createPanelHeader("Events", parent, false) : nullptr, + {}, + {}, + std::move(deleteItemHandler), + std::move(settingsItemHandler), + QString("Settings...")); } SavedNavigationView savedNavigationView(ScenarioAuthoringWidget::NavigationView view) { @@ -1095,11 +1160,82 @@ void ScenarioAuthoringWidget::refreshNavigationPanel() { [this](const QSet& expandedNodeIds) { crowdExpandedNodeIds_ = expandedNodeIds; }, + [this](const QString& crowdElementId) { + if (canvas_ != nullptr) { + canvas_->deleteCrowdElementById(crowdElementId); + } + }, shell_, shell_)); return; } - shell_->setNavigationPanel(createEventsPanel(layout_, currentScenario(), shell_, shell_)); + shell_->setNavigationPanel(createEventsPanel( + layout_, + currentScenario(), + shell_, + shell_, + [this](const QString& rawId) { + auto* scenario = currentScenario(); + if (scenario == nullptr || rawId.isEmpty()) { + return; + } + + const auto id = rawId.section('/', 0, 0); + if (canvas_ != nullptr && canvas_->deleteConnectionBlockById(id)) { + return; + } + if (canvas_ != nullptr && canvas_->deleteRouteGuidanceById(id)) { + return; + } + + const auto eventId = id.toStdString(); + auto& events = scenario->events; + const auto it = std::remove_if(events.begin(), events.end(), [&](const auto& event) { + return event.id == eventId; + }); + if (it == events.end()) { + return; + } + events.erase(it, events.end()); + scenario->draft.control.events = scenario->events; + recomputeDiffKeysAfterScenarioChanged(*scenario); + refreshNavigationPanel(); + refreshInspector(); + }, + [this](const QString& rawId) { + if (canvas_ == nullptr || rawId.isEmpty()) { + return; + } + + const auto id = rawId.section('/', 0, 0); + if (canvas_->editConnectionBlockScheduleById(id)) { + return; + } + if (canvas_->editRouteGuidanceById(id)) { + return; + } + + auto* scenario = currentScenario(); + if (scenario == nullptr) { + return; + } + + const auto eventId = id.toStdString(); + auto& events = scenario->events; + const auto it = std::find_if(events.begin(), events.end(), [&](auto& event) { + return event.id == eventId; + }); + if (it == events.end()) { + return; + } + if (!editOperationalEvent(&(*it), this)) { + return; + } + scenario->draft.control.events = scenario->events; + recomputeDiffKeysAfterScenarioChanged(*scenario); + refreshNavigationPanel(); + refreshInspector(); + })); } void ScenarioAuthoringWidget::refreshRightPanel() { diff --git a/src/application/ScenarioCanvasWidget.cpp b/src/application/ScenarioCanvasWidget.cpp index 922430f..eff0349 100644 --- a/src/application/ScenarioCanvasWidget.cpp +++ b/src/application/ScenarioCanvasWidget.cpp @@ -1333,6 +1333,76 @@ void ScenarioCanvasWidget::focusPlacement(const QString& placementId) { update(); } +bool ScenarioCanvasWidget::deleteCrowdElementById(const QString& crowdElementId) { + return deleteCrowdElement(crowdElementId); +} + +bool ScenarioCanvasWidget::deleteConnectionBlockById(const QString& blockId) { + auto it = std::find_if(connectionBlocks_.begin(), connectionBlocks_.end(), [&](const auto& block) { + return QString::fromStdString(block.id) == blockId; + }); + if (it == connectionBlocks_.end()) { + return false; + } + + connectionBlocks_.erase(it); + emitConnectionBlocksChanged(); + update(); + return true; +} + +bool ScenarioCanvasWidget::editConnectionBlockScheduleById(const QString& blockId) { + auto it = std::find_if(connectionBlocks_.begin(), connectionBlocks_.end(), [&](const auto& block) { + return QString::fromStdString(block.id) == blockId; + }); + if (it == connectionBlocks_.end()) { + return false; + } + + ConnectionBlockScheduleDialog dialog(it->intervals, this); + if (dialog.exec() != QDialog::Accepted) { + return false; + } + + it->intervals = dialog.intervals(); + emitConnectionBlocksChanged(); + update(); + return true; +} + +bool ScenarioCanvasWidget::deleteRouteGuidanceById(const QString& guidanceId) { + auto it = std::find_if(routeGuidances_.begin(), routeGuidances_.end(), [&](const auto& guidance) { + return QString::fromStdString(guidance.id) == guidanceId; + }); + if (it == routeGuidances_.end()) { + return false; + } + + routeGuidances_.erase(it); + emitRouteGuidancesChanged(); + update(); + return true; +} + +bool ScenarioCanvasWidget::editRouteGuidanceById(const QString& guidanceId) { + auto it = std::find_if(routeGuidances_.begin(), routeGuidances_.end(), [&](const auto& guidance) { + return QString::fromStdString(guidance.id) == guidanceId; + }); + if (it == routeGuidances_.end()) { + return false; + } + + RouteGuidanceSettingsDialog dialog(*it, this); + if (dialog.exec() != QDialog::Accepted) { + return false; + } + + *it = dialog.guidance(); + emitRouteGuidancesChanged(); + update(); + return true; +} + bool ScenarioCanvasWidget::eventFilter(QObject* watched, QEvent* event) { (void)watched; camera_.handleGlobalKeyEvent(event); @@ -2630,9 +2700,7 @@ void ScenarioCanvasWidget::openRouteGuidanceEditor(const QString& guidanceId, co } if (selected == deleteAction) { - routeGuidances_.erase(it); - emitRouteGuidancesChanged(); - update(); + deleteRouteGuidanceById(guidanceId); return; } diff --git a/src/application/ScenarioCanvasWidget.h b/src/application/ScenarioCanvasWidget.h index 77dd4f5..d5acca4 100644 --- a/src/application/ScenarioCanvasWidget.h +++ b/src/application/ScenarioCanvasWidget.h @@ -64,6 +64,11 @@ class ScenarioCanvasWidget : public QWidget { void focusLayoutElement(const QString& elementId); void activateLayoutElement(const QString& elementId); void focusPlacement(const QString& placementId); + bool deleteCrowdElementById(const QString& crowdElementId); + bool deleteConnectionBlockById(const QString& blockId); + bool editConnectionBlockScheduleById(const QString& blockId); + bool deleteRouteGuidanceById(const QString& guidanceId); + bool editRouteGuidanceById(const QString& guidanceId); protected: bool eventFilter(QObject* watched, QEvent* event) override;