Size things correctly.

Took 32 minutes

Took 10 seconds
This commit is contained in:
Lukas Brübach
2026-01-13 10:52:34 +01:00
parent b20d0bcb4f
commit b718c28dd5
4 changed files with 85 additions and 80 deletions

View File

@@ -14,34 +14,16 @@ BubbleWidget::BubbleWidget(QWidget *parent) : QFrame(parent)
counterLabel = new QLabel(this);
counterLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
closeButton = new QPushButton("", this);
closeButton->setFixedSize(20, 20);
textLabel = new QLabel(this);
textLabel->setWordWrap(true);
textLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
textLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
textLabel->setStyleSheet("color:black;"); // guard against global styles
// nav buttons
previousSequenceButton = new QPushButton("<<", this);
previousStepButton = new QPushButton("<", this);
nextStepButton = new QPushButton(">", this);
nextSequenceButton = new QPushButton(">>", this);
QHBoxLayout *navLayout = new QHBoxLayout;
navLayout->addStretch();
navLayout->addWidget(previousSequenceButton);
navLayout->addWidget(previousStepButton);
navLayout->addWidget(nextStepButton);
navLayout->addWidget(nextSequenceButton);
// Layout
layout->addWidget(counterLabel, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
layout->addItem(new QSpacerItem(10, 10, QSizePolicy::Expanding, QSizePolicy::Minimum), 0, 1);
layout->addWidget(closeButton, 0, 2, Qt::AlignRight);
layout->addWidget(textLabel, 1, 0, 1, 3);
layout->addLayout(navLayout, 2, 0, 1, 3);
// Make column 1 take extra space so text gets room to expand/wrap
layout->setColumnStretch(1, 1);

View File

@@ -11,11 +11,6 @@ class BubbleWidget : public QFrame
public:
QLabel *textLabel;
QLabel *counterLabel;
QPushButton *closeButton;
QPushButton *previousSequenceButton;
QPushButton *previousStepButton;
QPushButton *nextStepButton;
QPushButton *nextSequenceButton;
BubbleWidget(QWidget *parent);
void setText(const QString &text);

View File

@@ -9,21 +9,18 @@
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QResizeEvent>
TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent, Qt::Window)
{
setAttribute(Qt::WA_TransparentForMouseEvents, false);
setAttribute(Qt::WA_NoSystemBackground, true);
setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, false);
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint);
if (parent) {
parent->installEventFilter(this);
QRect r = parent->rect();
QPoint globalTopLeft = parent->mapToGlobal(QPoint(0, 0));
r.moveTopLeft(globalTopLeft);
setGeometry(r);
parentResized();
}
// ---- Control bar -------------------------------------------------
@@ -68,6 +65,16 @@ TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent, Qt::Window)
// ---- Bubble ------------------------------------------------------
bubble = new BubbleWidget(this);
bubble->hide();
controlBar->hide();
}
void TutorialOverlay::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
// Queue geometry correction after the window is fully shown
QMetaObject::invokeMethod(this, [this]() { parentResized(); }, Qt::QueuedConnection);
}
void TutorialOverlay::setTitle(const QString &title)
@@ -91,21 +98,9 @@ void TutorialOverlay::setTargetWidget(QWidget *w)
if (targetWidget)
targetWidget->installEventFilter(this);
updateHoleRect();
recomputeLayout();
}
void TutorialOverlay::updateHoleRect()
{
if (!targetWidget || !targetWidget->isVisible()) {
cachedHoleRect = QRect();
} else {
QPoint globalTopLeft = targetWidget->mapToGlobal(QPoint(0, 0));
QPoint localTopLeft = mapFromGlobal(globalTopLeft);
cachedHoleRect = QRect(localTopLeft, targetWidget->size()).adjusted(-6, -6, 6, 6);
}
}
void TutorialOverlay::setText(const QString &t)
{
tutorialText = t;
@@ -114,6 +109,16 @@ void TutorialOverlay::setText(const QString &t)
recomputeLayout();
}
QRect TutorialOverlay::currentHoleRect() const
{
if (!targetWidget || !targetWidget->isVisible())
return QRect();
QPoint globalTopLeft = targetWidget->mapToGlobal(QPoint(0, 0));
QPoint localTopLeft = mapFromGlobal(globalTopLeft);
return QRect(localTopLeft, targetWidget->size()).adjusted(-6, -6, 6, 6);
}
void TutorialOverlay::resizeEvent(QResizeEvent *)
{
recomputeLayout();
@@ -125,9 +130,13 @@ bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
parentResized();
}
if (obj == targetWidget && (event->type() == QEvent::Show || event->type() == QEvent::Hide)) {
updateHoleRect();
recomputeLayout();
if (obj == targetWidget) {
if (event->type() == QEvent::Show) {
// Defer layout recalculation to give Qt time to finalize geometry
QMetaObject::invokeMethod(this, [this]() { recomputeLayout(); }, Qt::QueuedConnection);
} else if (event->type() == QEvent::Hide || event->type() == QEvent::Move || event->type() == QEvent::Resize) {
recomputeLayout();
}
}
return QWidget::eventFilter(obj, event);
@@ -138,86 +147,101 @@ void TutorialOverlay::parentResized()
if (!parentWidget())
return;
QRect r = parentWidget()->rect();
QPoint globalTopLeft = parentWidget()->mapToGlobal(QPoint(0, 0));
QWidget *w = parentWidget();
// Get the parent rect in local coordinates
QRect r = w->rect();
// Map top-left to global coordinates
QPoint globalTopLeft = w->mapToGlobal(QPoint(0, 0));
// Set overlay geometry in screen coords, exactly matching the parent widget
r.moveTopLeft(globalTopLeft);
setGeometry(r);
updateHoleRect();
recomputeLayout();
}
// Recompute layout for bubble and control bar
void TutorialOverlay::recomputeLayout()
{
updateHoleRect();
QRect hole = currentHoleRect();
if (cachedHoleRect.isEmpty()) {
bubble->hide();
controlBar->hide();
if (hole.isEmpty()) {
if (bubble) {
bubble->hide();
}
if (controlBar) {
controlBar->hide();
}
hide();
return;
}
show();
raise();
bubble->show();
controlBar->show();
// ---- Bubble ----
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
highlightBubbleRect = computeBubbleRect(cachedHoleRect, bsize);
highlightBubbleRect = computeBubbleRect(hole, bsize);
bubble->setGeometry(highlightBubbleRect);
bubble->show();
bubble->raise();
// ---- Control bar ----
controlBar->adjustSize();
controlBar->show();
const int margin = 8;
QRect r = rect();
QList<QPoint> positions = {
{r.right() - controlBar->width() - margin, r.bottom() - controlBar->height() - margin}, // bottom-right
{r.right() - controlBar->width() - margin, margin}, // top-right
{margin, r.bottom() - controlBar->height() - margin}, // bottom-left
{margin, margin} // top-left
};
QList<QPoint> positions = {{r.right() - controlBar->width() - margin, r.bottom() - controlBar->height() - margin},
{r.right() - controlBar->width() - margin, margin},
{margin, r.bottom() - controlBar->height() - margin},
{margin, margin}};
for (const QPoint &pos : positions) {
QRect proposed(pos, controlBar->size());
if (!proposed.intersects(cachedHoleRect)) {
if (!proposed.intersects(hole)) {
controlBar->move(pos);
break;
}
}
controlBar->raise();
update();
}
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
{
const int margin = 16;
QRect r = rect();
QRect bubble;
if (hole.isEmpty()) {
return QRect(r.center().x() - bubbleSize.width() / 2, r.center().y() - bubbleSize.height() / 2,
bubbleSize.width(), bubbleSize.height());
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
} else {
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
if (!r.contains(bubble))
bubble.moveLeft(hole.left() - margin - bubbleSize.width());
if (!r.contains(bubble)) {
bubble.moveLeft(hole.center().x() - bubbleSize.width() / 2);
bubble.moveTop(hole.top() - margin - bubbleSize.height());
}
if (!r.contains(bubble))
bubble.moveTop(hole.bottom() + margin);
}
// Prefer right, left, top, bottom
QRect tryRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
if (r.contains(tryRect))
return tryRect;
// Final clamp to overlay bounds - ensure min <= max for qBound
int maxLeft = qMax(r.left(), r.right() - bubble.width());
int maxTop = qMax(r.top(), r.bottom() - bubble.height());
tryRect.moveLeft(hole.left() - margin - bubbleSize.width());
if (r.contains(tryRect))
return tryRect;
bubble.moveLeft(qBound(r.left(), bubble.left(), maxLeft));
bubble.moveTop(qBound(r.top(), bubble.top(), maxTop));
tryRect.moveLeft(hole.center().x() - bubbleSize.width() / 2);
tryRect.moveTop(hole.top() - margin - bubbleSize.height());
if (r.contains(tryRect))
return tryRect;
tryRect.moveTop(hole.bottom() + margin);
return tryRect;
return bubble;
}
void TutorialOverlay::paintEvent(QPaintEvent *)
@@ -227,9 +251,10 @@ void TutorialOverlay::paintEvent(QPaintEvent *)
p.fillRect(rect(), QColor(0, 0, 0, 160));
if (!cachedHoleRect.isEmpty()) {
QRect hole = currentHoleRect();
if (!hole.isEmpty()) {
QPainterPath holePath;
holePath.addRoundedRect(cachedHoleRect, 8, 8);
holePath.addRoundedRect(hole, 8, 8);
p.setCompositionMode(QPainter::CompositionMode_Clear);
p.fillPath(holePath, Qt::transparent);
}

View File

@@ -10,6 +10,8 @@ class QFrame;
class TutorialOverlay : public QWidget
{
Q_OBJECT
public slots:
void showEvent(QShowEvent *event) override;
public:
explicit TutorialOverlay(QWidget *parent = nullptr);
@@ -17,6 +19,7 @@ public:
void setTargetWidget(QWidget *w);
void updateHoleRect();
void setText(const QString &t);
QRect currentHoleRect() const;
void setTitle(const QString &title);
void setBlocking(bool blockInput);