Follow parent window properly.

Took 19 seconds
This commit is contained in:
Lukas Brübach 2026-05-23 17:02:56 +02:00
parent 7168802bbf
commit e9e3515628
4 changed files with 112 additions and 65 deletions

View file

@ -17,6 +17,10 @@ public:
void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal); void setProgress(int stepNum, int totalSteps, int overallStep, int overallTotal);
void setInteractionHint(const QString &hint); void setInteractionHint(const QString &hint);
void setValidationHint(const QString &hint); void setValidationHint(const QString &hint);
QGridLayout *getLayout() const
{
return layout;
}
private: private:
void clearValidationHint(); void clearValidationHint();

View file

@ -38,12 +38,8 @@ void TutorialController::start()
QTimer::singleShot(0, this, [this]() { QTimer::singleShot(0, this, [this]() {
QWidget *win = tutorializedWidget->window(); QWidget *win = tutorializedWidget->window();
// Reparent to make absolutely sure tutorialOverlay->setParent(win); // triggers changeEvent and installs filter
tutorialOverlay->setParent(win); tutorialOverlay->setGeometry(win->rect());
tutorialOverlay->setGeometry(0, 0, win->width(), win->height());
// Stack order
tutorialOverlay->stackUnder(nullptr);
tutorialOverlay->show(); tutorialOverlay->show();
tutorialOverlay->raise(); tutorialOverlay->raise();

View file

@ -22,8 +22,8 @@ TutorialOverlay::TutorialOverlay(QWidget *parent) : QWidget(parent)
{ {
setAttribute(Qt::WA_TranslucentBackground, true); setAttribute(Qt::WA_TranslucentBackground, true);
setAttribute(Qt::WA_TransparentForMouseEvents, false); setAttribute(Qt::WA_TransparentForMouseEvents, false);
setAttribute(Qt::WA_StaticContents, false);
setAttribute(Qt::WA_OpaquePaintEvent, false); setAttribute(Qt::WA_NoSystemBackground, true);
setAutoFillBackground(false); setAutoFillBackground(false);
if (parent) { if (parent) {
@ -81,14 +81,28 @@ void TutorialOverlay::showEvent(QShowEvent *event)
QWidget::showEvent(event); QWidget::showEvent(event);
if (parentWidget()) { if (parentWidget()) {
QWidget *parent = parentWidget(); setGeometry(0, 0, parentWidget()->width(), parentWidget()->height());
setGeometry(0, 0, parent->width(), parent->height());
} }
raise(); raise();
parentResized(); parentResized();
} }
void TutorialOverlay::changeEvent(QEvent *event)
{
if (event->type() == QEvent::ParentChange) {
// Remove filter from old parent (Qt has already changed parentWidget() by now,
// so iterate sender list isn't easy — reinstall unconditionally on the new parent)
QWidget *p = parentWidget();
if (p) {
p->installEventFilter(this); // safe to call multiple times
setGeometry(p->rect());
raise();
}
}
QWidget::changeEvent(event);
}
void TutorialOverlay::setTitle(const QString &title) void TutorialOverlay::setTitle(const QString &title)
{ {
titleLabel->setText(title); titleLabel->setText(title);
@ -107,23 +121,14 @@ void TutorialOverlay::setInteractive(bool interactive, bool clickThrough)
if (nextButton) { if (nextButton) {
nextButton->setEnabled(!interactive); nextButton->setEnabled(!interactive);
if (interactive) { nextButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next step");
nextButton->setToolTip("Complete the highlighted action to continue");
} else {
nextButton->setToolTip("Next step");
}
} }
if (nextSeqButton) { if (nextSeqButton) {
nextSeqButton->setEnabled(!interactive); nextSeqButton->setEnabled(!interactive);
if (interactive) { nextSeqButton->setToolTip(interactive ? "Complete the highlighted action to continue" : "Next chapter");
nextSeqButton->setToolTip("Complete the highlighted action to continue");
} else {
nextSeqButton->setToolTip("Next chapter");
}
} }
// Update mask when clickThrough changes
updateMask(); updateMask();
} }
@ -154,6 +159,7 @@ void TutorialOverlay::setProgress(int stepNum,
void TutorialOverlay::setTargetWidget(QWidget *w) void TutorialOverlay::setTargetWidget(QWidget *w)
{ {
if (targetWidget) { if (targetWidget) {
targetWidget->removeEventFilter(this); targetWidget->removeEventFilter(this);
} }
@ -169,6 +175,7 @@ void TutorialOverlay::setTargetWidget(QWidget *w)
void TutorialOverlay::setText(const QString &t) void TutorialOverlay::setText(const QString &t)
{ {
tutorialText = t; tutorialText = t;
bubble->setText(t); bubble->setText(t);
bubble->adjustSize(); bubble->adjustSize();
@ -181,56 +188,54 @@ QRect TutorialOverlay::currentHoleRect() const
return QRect(); return QRect();
} }
QPoint targetGlobal = targetWidget->mapToGlobal(QPoint(0, 0)); QPoint p = targetWidget->mapToGlobal(QPoint(0, 0));
QPoint targetInOverlay = mapFromGlobal(targetGlobal); QPoint local = mapFromGlobal(p);
return QRect(targetInOverlay, targetWidget->size()).adjusted(-6, -6, 6, 6); QRect r(local, targetWidget->size());
r = r.adjusted(-6, -6, 6, 6);
return r;
} }
void TutorialOverlay::mousePressEvent(QMouseEvent *event) void TutorialOverlay::mousePressEvent(QMouseEvent *event)
{ {
QRect hole = currentHoleRect(); QRect hole = currentHoleRect();
// Check if click is in the highlighted area
if (hole.contains(event->pos())) { if (hole.contains(event->pos())) {
// For non-clickthrough steps, emit targetClicked for advancement
if (!allowClickThrough && isInteractive && !qobject_cast<QLineEdit *>(targetWidget) && if (!allowClickThrough && isInteractive && !qobject_cast<QLineEdit *>(targetWidget) &&
!qobject_cast<QTextEdit *>(targetWidget) && !qobject_cast<QPlainTextEdit *>(targetWidget) && !qobject_cast<QTextEdit *>(targetWidget) && !qobject_cast<QPlainTextEdit *>(targetWidget) &&
!qobject_cast<QComboBox *>(targetWidget)) { !qobject_cast<QComboBox *>(targetWidget)) {
QTimer::singleShot(100, this, [this]() { emit targetClicked(); }); QTimer::singleShot(100, this, [this]() { emit targetClicked(); });
} }
// If allowClickThrough, the mask ensures events pass through
return; return;
} }
// Click outside highlighted area - block it
event->accept(); event->accept();
} }
void TutorialOverlay::updateMask() void TutorialOverlay::updateMask()
{ {
if (allowClickThrough) { clearMask();
QRect hole = currentHoleRect();
if (!hole.isEmpty()) { if (!isVisible() || !parentWidget()) {
// Create a mask that excludes the hole area return;
QRegion fullRegion(rect()); }
QRegion holeRegion(hole); if (!allowClickThrough) {
QRegion maskRegion = fullRegion.subtracted(holeRegion); return;
setMask(maskRegion); }
} else {
clearMask(); QRect hole = currentHoleRect();
}
} else { if (!hole.isEmpty()) {
clearMask(); QRegion full(rect());
QRegion cut(hole);
setMask(full.subtracted(cut));
} }
} }
bool TutorialOverlay::event(QEvent *event) bool TutorialOverlay::event(QEvent *event)
{ {
// Update mask on any event that might change geometry
if (event->type() == QEvent::Move || event->type() == QEvent::Resize) {
updateMask();
}
return QWidget::event(event); return QWidget::event(event);
} }
@ -241,15 +246,29 @@ void TutorialOverlay::resizeEvent(QResizeEvent *)
bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event) bool TutorialOverlay::eventFilter(QObject *obj, QEvent *event)
{ {
if (obj == parentWidget() && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) { if (obj == parentWidget()) {
parentResized(); switch (event->type()) {
case QEvent::Resize:
case QEvent::Move: // window dragged to another monitor / position
parentResized();
break;
default:
break;
}
} }
if (obj == targetWidget) { if (obj == targetWidget) {
if (event->type() == QEvent::Show) { switch (event->type()) {
QMetaObject::invokeMethod(this, [this]() { recomputeLayout(); }, Qt::QueuedConnection); case QEvent::Show:
} else if (event->type() == QEvent::Hide || event->type() == QEvent::Move || event->type() == QEvent::Resize) { QMetaObject::invokeMethod(this, [this] { recomputeLayout(); }, Qt::QueuedConnection);
recomputeLayout(); break;
case QEvent::Hide:
case QEvent::Move:
case QEvent::Resize:
recomputeLayout();
break;
default:
break;
} }
} }
@ -262,22 +281,38 @@ void TutorialOverlay::parentResized()
return; return;
} }
setGeometry(0, 0, parentWidget()->width(), parentWidget()->height()); const QSize s = parentWidget()->size();
setGeometry(0, 0, s.width(), s.height());
clearMask();
recomputeLayout(); recomputeLayout();
setAttribute(Qt::WA_NoSystemBackground, false);
setAttribute(Qt::WA_NoSystemBackground, true);
update();
} }
void TutorialOverlay::recomputeLayout() void TutorialOverlay::recomputeLayout()
{ {
if (!parentWidget()) {
return;
}
resize(parentWidget()->window()->geometry().size());
bubble->adjustSize();
QRect hole = currentHoleRect(); QRect hole = currentHoleRect();
if (hole.isEmpty()) { if (hole.isEmpty()) {
if (bubble) { bubble->hide();
bubble->hide(); controlBar->hide();
}
if (controlBar) {
controlBar->hide();
}
hide(); hide();
update();
return; return;
} }
@ -286,9 +321,9 @@ void TutorialOverlay::recomputeLayout()
QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60)); QSize bsize = bubble->sizeHint().expandedTo(QSize(160, 60));
highlightBubbleRect = computeBubbleRect(hole, bsize); highlightBubbleRect = computeBubbleRect(hole, bsize);
bubble->setGeometry(highlightBubbleRect); bubble->setGeometry(highlightBubbleRect);
bubble->show(); bubble->show();
bubble->raise();
controlBar->adjustSize(); controlBar->adjustSize();
controlBar->show(); controlBar->show();
@ -301,28 +336,39 @@ void TutorialOverlay::recomputeLayout()
{margin, r.bottom() - controlBar->height() - margin}, {margin, r.bottom() - controlBar->height() - margin},
{margin, margin}}; {margin, margin}};
QRect placedRect;
for (const QPoint &pos : positions) { for (const QPoint &pos : positions) {
QRect proposed(pos, controlBar->size()); QRect proposed(pos, controlBar->size());
if (!proposed.intersects(hole)) {
controlBar->move(pos); if (!proposed.intersects(hole) && !proposed.intersects(highlightBubbleRect)) {
placedRect = proposed;
break; break;
} }
} }
controlBar->raise(); if (!placedRect.isValid()) {
placedRect = QRect(QPoint(margin, margin), controlBar->size());
}
controlBar->move(placedRect.topLeft());
controlBar->show();
updateMask();
update(); update();
updateMask(); // Update mask after layout changes
} }
QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const QRect TutorialOverlay::computeBubbleRect(const QRect &hole, const QSize &bubbleSize) const
{ {
const int margin = 16;
QRect r = rect(); QRect r = rect();
QRect bubble; QRect bubble;
if (hole.isEmpty()) { if (hole.isEmpty()) {
bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize); bubble = QRect(r.center() - QPoint(bubbleSize.width() / 2, bubbleSize.height() / 2), bubbleSize);
} else { } else {
const int margin = 16;
bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height()); bubble = QRect(hole.right() + margin, hole.top(), bubbleSize.width(), bubbleSize.height());
if (!r.contains(bubble)) { if (!r.contains(bubble)) {

View file

@ -26,6 +26,7 @@ public:
void parentResized(); void parentResized();
QRect currentHoleRect() const; QRect currentHoleRect() const;
bool eventFilter(QObject *obj, QEvent *event) override;
signals: signals:
void nextStep(); void nextStep();
@ -42,7 +43,7 @@ protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void updateMask(); void updateMask();
bool eventFilter(QObject *obj, QEvent *event) override; void changeEvent(QEvent *event) override;
private: private:
void recomputeLayout(); void recomputeLayout();