| 341 | } |
| 342 | |
| 343 | void ClientChainWidget::paintEvent(QPaintEvent*) |
| 344 | { |
| 345 | rebuildLayout(); |
| 346 | |
| 347 | QPainter p(this); |
| 348 | p.setRenderHint(QPainter::Antialiasing, true); |
| 349 | p.fillRect(rect(), AetherSDR::ThemeManager::instance().color("color.background.0")); |
| 350 | |
| 351 | if (m_boxes.isEmpty()) return; |
| 352 | |
| 353 | // Connector lines between adjacent boxes — handles three cases: |
| 354 | // - same row, both visual-left-to-right (even row) |
| 355 | // - same row, both visual-right-to-left (odd row) |
| 356 | // - row transition: a small L / U bend with a downward arrow |
| 357 | auto drawArrowHead = [&](QPointF tip, QPointF from) { |
| 358 | // Filled triangle pointing from `from` toward `tip`. |
| 359 | const QPointF dir = tip - from; |
| 360 | const qreal len = std::hypot(dir.x(), dir.y()); |
| 361 | if (len < 0.01) return; |
| 362 | const QPointF u(dir.x() / len, dir.y() / len); |
| 363 | const QPointF perp(-u.y(), u.x()); |
| 364 | QPainterPath head; |
| 365 | head.moveTo(tip); |
| 366 | head.lineTo(tip.x() - u.x() * kArrowTip + perp.x() * kArrowTip, |
| 367 | tip.y() - u.y() * kArrowTip + perp.y() * kArrowTip); |
| 368 | head.lineTo(tip.x() - u.x() * kArrowTip - perp.x() * kArrowTip, |
| 369 | tip.y() - u.y() * kArrowTip - perp.y() * kArrowTip); |
| 370 | head.closeSubpath(); |
| 371 | p.setBrush(kConnector()); |
| 372 | p.setPen(Qt::NoPen); |
| 373 | p.drawPath(head); |
| 374 | }; |
| 375 | |
| 376 | p.setPen(QPen(kConnector(), 2.0)); |
| 377 | for (int i = 0; i + 1 < m_boxes.size(); ++i) { |
| 378 | const QRectF a = m_boxes[i].rect; |
| 379 | const QRectF b = m_boxes[i + 1].rect; |
| 380 | const bool sameRow = qFuzzyCompare(a.center().y(), b.center().y()); |
| 381 | if (sameRow) { |
| 382 | // Horizontal connector — direction depends on row parity. |
| 383 | // Even row: a is left, b is right → arrow into b's left edge. |
| 384 | // Odd row: a is right, b is left → arrow into b's right edge. |
| 385 | const bool rtl = a.center().x() > b.center().x(); |
| 386 | QPointF from, to; |
| 387 | if (rtl) { |
| 388 | from = QPointF(a.left(), a.center().y()); |
| 389 | to = QPointF(b.right() + 1, b.center().y()); |
| 390 | } else { |
| 391 | from = QPointF(a.right(), a.center().y()); |
| 392 | to = QPointF(b.left() - 1, b.center().y()); |
| 393 | } |
| 394 | p.setPen(QPen(kConnector(), 2.0)); |
| 395 | p.drawLine(from, to); |
| 396 | drawArrowHead(to, from); |
| 397 | } else { |
| 398 | // Row transition — snake layout always turns at whichever |
| 399 | // side of the widget the boxes currently occupy. Compare |
| 400 | // the box centre to the widget centre: right half → turn |
nothing calls this directly
no test coverage detected