Skip to content

Commit ef3e5ec

Browse files
authored
[Application] 경로 차단 스케줄 툴팁 표시
Adds hover tooltips for connection block schedules on scenario and simulation canvases.
1 parent 622dace commit ef3e5ec

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

src/application/ScenarioCanvasWidget.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <QPushButton>
2727
#include <QSpinBox>
2828
#include <QToolButton>
29+
#include <QToolTip>
2930
#include <QVBoxLayout>
3031
#include <QWheelEvent>
3132

@@ -55,6 +56,66 @@ bool matchesFloor(const std::string& elementFloorId, const QString& floorId) {
5556
return floorId.isEmpty() || elementFloorId.empty() || QString::fromStdString(elementFloorId) == floorId;
5657
}
5758

59+
QString formatConnectionBlockTooltip(const safecrowd::domain::ConnectionBlockDraft& block) {
60+
if (block.connectionId.empty()) {
61+
return {};
62+
}
63+
64+
QString text = QStringLiteral("Block schedule");
65+
if (block.intervals.empty()) {
66+
text.append("\n- Always");
67+
return text;
68+
}
69+
70+
for (const auto& interval : block.intervals) {
71+
const auto start = std::max(0.0, interval.startSeconds);
72+
const auto end = std::max(start, interval.endSeconds);
73+
text.append(QString("\n- %1s ~ %2s").arg(start, 0, 'f', 1).arg(end, 0, 'f', 1));
74+
}
75+
return text;
76+
}
77+
78+
std::optional<std::size_t> hoveredConnectionBlockIndex(
79+
const safecrowd::domain::FacilityLayout2D& layout,
80+
const std::vector<safecrowd::domain::ConnectionBlockDraft>& blocks,
81+
const LayoutCanvasTransform& transform,
82+
const QString& currentFloorId,
83+
const QPointF& screenPosition) {
84+
constexpr double kHoverRadiusPixels = 14.0;
85+
86+
std::optional<std::size_t> closestIndex;
87+
double closestDistanceSq = kHoverRadiusPixels * kHoverRadiusPixels;
88+
89+
for (std::size_t index = 0; index < blocks.size(); ++index) {
90+
const auto& block = blocks[index];
91+
if (block.connectionId.empty()) {
92+
continue;
93+
}
94+
95+
const auto it = std::find_if(layout.connections.begin(), layout.connections.end(), [&](const auto& connection) {
96+
return connection.id == block.connectionId;
97+
});
98+
if (it == layout.connections.end()) {
99+
continue;
100+
}
101+
if (!matchesFloor(it->floorId, currentFloorId)) {
102+
continue;
103+
}
104+
105+
const auto center = transform.map({.x = (it->centerSpan.start.x + it->centerSpan.end.x) * 0.5,
106+
.y = (it->centerSpan.start.y + it->centerSpan.end.y) * 0.5});
107+
const auto dx = center.x() - screenPosition.x();
108+
const auto dy = center.y() - screenPosition.y();
109+
const auto distanceSq = (dx * dx) + (dy * dy);
110+
if (distanceSq <= closestDistanceSq) {
111+
closestDistanceSq = distanceSq;
112+
closestIndex = index;
113+
}
114+
}
115+
116+
return closestIndex;
117+
}
118+
58119
QString defaultFloorId(const safecrowd::domain::FacilityLayout2D& layout) {
59120
if (!layout.floors.empty() && !layout.floors.front().id.empty()) {
60121
return QString::fromStdString(layout.floors.front().id);
@@ -699,6 +760,12 @@ void ScenarioCanvasWidget::keyReleaseEvent(QKeyEvent* event) {
699760
QWidget::keyReleaseEvent(event);
700761
}
701762

763+
void ScenarioCanvasWidget::leaveEvent(QEvent* event) {
764+
hoveredConnectionBlockId_.clear();
765+
QToolTip::hideText();
766+
QWidget::leaveEvent(event);
767+
}
768+
702769
void ScenarioCanvasWidget::mouseDoubleClickEvent(QMouseEvent* event) {
703770
if (event->button() == Qt::LeftButton) {
704771
camera_.reset();
@@ -716,17 +783,46 @@ void ScenarioCanvasWidget::mouseMoveEvent(QMouseEvent* event) {
716783
}
717784

718785
if (dragging_) {
786+
if (!hoveredConnectionBlockId_.isEmpty()) {
787+
hoveredConnectionBlockId_.clear();
788+
QToolTip::hideText();
789+
}
719790
dragCurrent_ = event->position();
720791
update();
721792
event->accept();
722793
return;
723794
}
724795
if (selectionDragging_) {
796+
if (!hoveredConnectionBlockId_.isEmpty()) {
797+
hoveredConnectionBlockId_.clear();
798+
QToolTip::hideText();
799+
}
725800
selectionDragCurrent_ = event->position();
726801
update();
727802
event->accept();
728803
return;
729804
}
805+
806+
if (const auto bounds = collectBounds(); bounds.has_value()) {
807+
const auto transform = currentTransform(*bounds);
808+
const auto hoveredIndex = hoveredConnectionBlockIndex(layout_, connectionBlocks_, transform, currentFloorId_, event->position());
809+
if (!hoveredIndex.has_value()) {
810+
if (!hoveredConnectionBlockId_.isEmpty()) {
811+
hoveredConnectionBlockId_.clear();
812+
QToolTip::hideText();
813+
}
814+
} else {
815+
const auto& block = connectionBlocks_[*hoveredIndex];
816+
const auto tooltip = formatConnectionBlockTooltip(block);
817+
if (!tooltip.isEmpty()) {
818+
const auto hoveredId = QString::fromStdString(block.id.empty() ? block.connectionId : block.id);
819+
if (hoveredId != hoveredConnectionBlockId_) {
820+
hoveredConnectionBlockId_ = hoveredId;
821+
QToolTip::showText(event->globalPosition().toPoint(), tooltip, this);
822+
}
823+
}
824+
}
825+
}
730826
QWidget::mouseMoveEvent(event);
731827
}
732828

src/application/ScenarioCanvasWidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class ScenarioCanvasWidget : public QWidget {
6767
bool eventFilter(QObject* watched, QEvent* event) override;
6868
void keyPressEvent(QKeyEvent* event) override;
6969
void keyReleaseEvent(QKeyEvent* event) override;
70+
void leaveEvent(QEvent* event) override;
7071
void mouseDoubleClickEvent(QMouseEvent* event) override;
7172
void mouseMoveEvent(QMouseEvent* event) override;
7273
void mousePressEvent(QMouseEvent* event) override;
@@ -145,6 +146,7 @@ class ScenarioCanvasWidget : public QWidget {
145146
QSpinBox* groupCountSpinBox_{nullptr};
146147
QLabel* groupDistributionLabel_{nullptr};
147148
QComboBox* groupDistributionComboBox_{nullptr};
149+
QString hoveredConnectionBlockId_{};
148150
std::function<void(const QString&)> layoutElementActivatedHandler_{};
149151
std::function<void(const QString&)> crowdSelectionChangedHandler_{};
150152
std::function<void(const std::vector<ScenarioCrowdPlacement>&)> placementsChangedHandler_{};

src/application/SimulationCanvasWidget.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <QRadialGradient>
1717
#include <QResizeEvent>
1818
#include <QSignalBlocker>
19+
#include <QToolTip>
1920
#include <QWheelEvent>
2021

2122
namespace safecrowd::application {
@@ -93,6 +94,65 @@ QColor densityHeatmapColor(double ratio, int alpha) {
9394
return QColor(220, 38, 38, alpha);
9495
}
9596

97+
QString formatScheduleTooltip(const safecrowd::domain::ConnectionBlockDraft& block) {
98+
if (block.connectionId.empty()) {
99+
return {};
100+
}
101+
102+
QString text = QStringLiteral("Block schedule");
103+
if (block.intervals.empty()) {
104+
text.append("\n- Always");
105+
return text;
106+
}
107+
108+
for (const auto& interval : block.intervals) {
109+
const auto start = std::max(0.0, interval.startSeconds);
110+
const auto end = std::max(start, interval.endSeconds);
111+
text.append(QString("\n- %1s ~ %2s").arg(start, 0, 'f', 1).arg(end, 0, 'f', 1));
112+
}
113+
return text;
114+
}
115+
116+
std::optional<std::size_t> hoveredBlockedConnectionIndex(
117+
const safecrowd::domain::FacilityLayout2D& layout,
118+
const std::vector<safecrowd::domain::ConnectionBlockDraft>& blocks,
119+
const LayoutCanvasTransform& transform,
120+
const std::string& currentFloorId,
121+
double elapsedSeconds,
122+
const QPointF& screenPosition) {
123+
constexpr double kHoverRadiusPixels = 14.0;
124+
125+
std::optional<std::size_t> closestIndex;
126+
double closestDistanceSq = kHoverRadiusPixels * kHoverRadiusPixels;
127+
128+
for (std::size_t index = 0; index < blocks.size(); ++index) {
129+
const auto& block = blocks[index];
130+
if (!connectionShouldBeBlocked(block, elapsedSeconds)) {
131+
continue;
132+
}
133+
const auto it = std::find_if(layout.connections.begin(), layout.connections.end(), [&](const auto& connection) {
134+
return connection.id == block.connectionId;
135+
});
136+
if (it == layout.connections.end()) {
137+
continue;
138+
}
139+
if (!matchesFloor(it->floorId, currentFloorId)) {
140+
continue;
141+
}
142+
143+
const auto center = transform.map(connectionCenter(*it));
144+
const auto dx = center.x() - screenPosition.x();
145+
const auto dy = center.y() - screenPosition.y();
146+
const auto distanceSq = (dx * dx) + (dy * dy);
147+
if (distanceSq <= closestDistanceSq) {
148+
closestDistanceSq = distanceSq;
149+
closestIndex = index;
150+
}
151+
}
152+
153+
return closestIndex;
154+
}
155+
96156
} // namespace
97157

98158
SimulationCanvasWidget::SimulationCanvasWidget(safecrowd::domain::FacilityLayout2D layout, QWidget* parent)
@@ -211,6 +271,12 @@ void SimulationCanvasWidget::keyReleaseEvent(QKeyEvent* event) {
211271
QWidget::keyReleaseEvent(event);
212272
}
213273

274+
void SimulationCanvasWidget::leaveEvent(QEvent* event) {
275+
hoveredConnectionBlockId_.clear();
276+
QToolTip::hideText();
277+
QWidget::leaveEvent(event);
278+
}
279+
214280
void SimulationCanvasWidget::mouseDoubleClickEvent(QMouseEvent* event) {
215281
if (event->button() == Qt::LeftButton) {
216282
camera_.reset();
@@ -228,6 +294,48 @@ void SimulationCanvasWidget::mouseMoveEvent(QMouseEvent* event) {
228294
update();
229295
return;
230296
}
297+
298+
const auto bounds = collectBounds();
299+
if (!bounds.has_value()) {
300+
if (!hoveredConnectionBlockId_.empty()) {
301+
hoveredConnectionBlockId_.clear();
302+
QToolTip::hideText();
303+
}
304+
QWidget::mouseMoveEvent(event);
305+
return;
306+
}
307+
308+
const auto transform = currentTransform(*bounds);
309+
const auto elapsedSeconds = std::max(0.0, frame_.elapsedSeconds);
310+
const auto hoveredIndex = hoveredBlockedConnectionIndex(
311+
layout_,
312+
connectionBlocks_,
313+
transform,
314+
currentFloorId_,
315+
elapsedSeconds,
316+
event->position());
317+
318+
if (!hoveredIndex.has_value()) {
319+
if (!hoveredConnectionBlockId_.empty()) {
320+
hoveredConnectionBlockId_.clear();
321+
QToolTip::hideText();
322+
}
323+
QWidget::mouseMoveEvent(event);
324+
return;
325+
}
326+
327+
const auto& block = connectionBlocks_[*hoveredIndex];
328+
const auto tooltip = formatScheduleTooltip(block);
329+
if (tooltip.isEmpty()) {
330+
QWidget::mouseMoveEvent(event);
331+
return;
332+
}
333+
334+
const auto hoveredId = block.id.empty() ? block.connectionId : block.id;
335+
if (hoveredId != hoveredConnectionBlockId_) {
336+
hoveredConnectionBlockId_ = hoveredId;
337+
QToolTip::showText(event->globalPosition().toPoint(), tooltip, this);
338+
}
231339
QWidget::mouseMoveEvent(event);
232340
}
233341

src/application/SimulationCanvasWidget.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class SimulationCanvasWidget : public QWidget {
5151
bool eventFilter(QObject* watched, QEvent* event) override;
5252
void keyPressEvent(QKeyEvent* event) override;
5353
void keyReleaseEvent(QKeyEvent* event) override;
54+
void leaveEvent(QEvent* event) override;
5455
void mouseDoubleClickEvent(QMouseEvent* event) override;
5556
void mouseMoveEvent(QMouseEvent* event) override;
5657
void mousePressEvent(QMouseEvent* event) override;
@@ -95,6 +96,8 @@ class SimulationCanvasWidget : public QWidget {
9596
double layoutCacheZoom_{0.0};
9697
double layoutCacheDevicePixelRatio_{0.0};
9798
bool layoutCacheValid_{false};
99+
100+
std::string hoveredConnectionBlockId_{};
98101
};
99102

100103
} // namespace safecrowd::application

0 commit comments

Comments
 (0)