13#include <QPainterPath>
31 resize(parent->size());
32 parent->installEventFilter(
this);
42 m_resolver = std::move(resolver);
48 parentWidget()->removeEventFilter(
this);
52 resize(newParent->size());
53 newParent->installEventFilter(
this);
57void TourOverlay::setupUi()
60 m_callout =
new QFrame(
this);
63 auto *layout =
new QVBoxLayout(m_callout);
64 layout->setContentsMargins(14, 14, 14, 14);
65 layout->setSpacing(8);
68 auto *topRow =
new QHBoxLayout;
69 m_stepCounter =
new QLabel(m_callout);
70 topRow->addWidget(m_stepCounter);
72 layout->addLayout(topRow);
74 m_titleLabel =
new QLabel(m_callout);
75 m_titleLabel->setWordWrap(
true);
76 layout->addWidget(m_titleLabel);
78 m_bodyLabel =
new QLabel(m_callout);
79 m_bodyLabel->setWordWrap(
true);
80 layout->addWidget(m_bodyLabel);
83 auto *btnRow =
new QHBoxLayout;
84 btnRow->setSpacing(6);
86 m_closeButton =
new QPushButton(tr(
"Exit"), m_callout);
87 m_closeButton->setToolTip(tr(
"Stop the tour"));
89 m_prevButton =
new QPushButton(tr(
"← Back"), m_callout);
91 m_nextButton =
new QPushButton(tr(
"Next →"), m_callout);
93 btnRow->addWidget(m_closeButton);
94 btnRow->addWidget(m_prevButton);
96 btnRow->addWidget(m_nextButton);
97 layout->addLayout(btnRow);
99 m_callout->adjustSize();
102 connect(m_prevButton, &QPushButton::clicked,
this, [
this] { m_engine->
goToPreviousStep(); });
103 connect(m_nextButton, &QPushButton::clicked,
this, [
this] { m_engine->advanceStep(); });
108int TourOverlay::scaledFontPx(
int basePx)
110 constexpr int baselinePx = 13;
111 const int currentPx = QFontInfo(QApplication::font()).pixelSize();
112 return qRound(
static_cast<double>(basePx) * currentPx / baselinePx);
115void TourOverlay::applyTheme()
118 const int navFontPx = scaledFontPx(13);
119 const int counterFontPx = scaledFontPx(12);
120 const int titleFontPx = scaledFontPx(16);
121 const int bodyFontPx = scaledFontPx(14);
124 m_dimColor = QColor(0, 0, 0, 160);
125 m_spotlightColor = QColor(255, 200, 50, 220);
127 m_callout->setStyleSheet(
128 QString(
"QFrame { background: rgba(20,20,20,240); border: 1px solid rgba(255,200,50,120); border-radius: 8px; } QLabel { background: transparent; border: none; } QPushButton { border-radius: 4px; padding: 4px 12px; font-size: %1px; }").arg(navFontPx)
130 m_stepCounter->setStyleSheet(QString(
"color: rgba(255,200,50,200); font-size: %1px;").arg(counterFontPx));
131 m_titleLabel->setStyleSheet(QString(
"color: white; font-size: %1px; font-weight: bold;").arg(titleFontPx));
132 m_bodyLabel->setStyleSheet(QString(
"color: rgba(220,220,220,230); font-size: %1px;").arg(bodyFontPx));
134 const QString navStyle =
135 QString(
"QPushButton { color: white; background: rgba(255,255,255,40); border: 1px solid rgba(255,255,255,80); border-radius: 4px; padding: 4px 12px; font-size: %1px; } QPushButton:hover { background: rgba(255,255,255,60); } QPushButton:disabled { color: rgba(255,255,255,80); background: rgba(80,80,80,60); }").arg(navFontPx);
136 m_closeButton->setStyleSheet(navStyle);
137 m_prevButton->setStyleSheet(navStyle);
138 m_nextButton->setStyleSheet(
139 QString(
"QPushButton { color: white; background: rgba(255,160,20,200); border: 1px solid rgba(255,200,50,150); border-radius: 4px; padding: 4px 12px; font-size: %1px; } QPushButton:hover { background: rgba(255,180,40,230); }").arg(navFontPx));
141 m_dimColor = QColor(0, 0, 0, 90);
142 m_spotlightColor = QColor(0, 120, 210, 220);
144 m_callout->setStyleSheet(
145 QString(
"QFrame { background: rgba(255,255,255,240); border: 1px solid rgba(0,100,200,150); border-radius: 8px; } QLabel { background: transparent; border: none; } QPushButton { border-radius: 4px; padding: 4px 12px; font-size: %1px; }").arg(navFontPx)
147 m_stepCounter->setStyleSheet(QString(
"color: rgba(0,100,200,200); font-size: %1px;").arg(counterFontPx));
148 m_titleLabel->setStyleSheet(QString(
"color: rgb(20,20,20); font-size: %1px; font-weight: bold;").arg(titleFontPx));
149 m_bodyLabel->setStyleSheet(QString(
"color: rgba(50,50,50,230); font-size: %1px;").arg(bodyFontPx));
151 const QString navStyle =
152 QString(
"QPushButton { color: rgb(20,20,20); background: rgba(0,0,0,20); border: 1px solid rgba(0,0,0,80); border-radius: 4px; padding: 4px 12px; font-size: %1px; } QPushButton:hover { background: rgba(0,0,0,40); } QPushButton:disabled { color: rgba(0,0,0,60); background: rgba(0,0,0,10); }").arg(navFontPx);
153 m_closeButton->setStyleSheet(navStyle);
154 m_prevButton->setStyleSheet(navStyle);
155 m_nextButton->setStyleSheet(
156 QString(
"QPushButton { color: white; background: rgba(0,120,210,200); border: 1px solid rgba(0,100,200,150); border-radius: 4px; padding: 4px 12px; font-size: %1px; } QPushButton:hover { background: rgba(0,140,230,230); }").arg(navFontPx));
164 if (obj == parentWidget() && event->type() == QEvent::Resize) {
165 resize(parentWidget()->size());
168 return QWidget::eventFilter(obj, event);
175 m_stepCounter->setText(tr(
"Step %1 of %2").arg(step + 1).arg(total));
176 m_titleLabel->setText(stepData.
title);
177 m_bodyLabel->setText(stepData.
body);
179 m_prevButton->setEnabled(step > 0);
180 m_nextButton->setText(step == total - 1 ? tr(
"Finish") : tr(
"Next →"));
182 m_currentTarget = stepData.
target;
183 m_highlightRect = QRect{};
184 if (m_resolver && !stepData.
target.isEmpty() && stepData.
target !=
"none") {
185 m_highlightRect = m_resolver(stepData.
target);
188 m_callout->adjustSize();
199 const TourStep &stepData = m_engine->currentStepData();
200 const int step = m_engine->currentStep();
201 const int total = m_engine->totalSteps();
203 m_stepCounter->setText(tr(
"Step %1 of %2").arg(step + 1).arg(total));
204 m_titleLabel->setText(stepData.
title);
205 m_bodyLabel->setText(stepData.
body);
206 m_nextButton->setText(step == total - 1 ? tr(
"Finish") : tr(
"Next →"));
208 m_closeButton->setText(tr(
"Exit"));
209 m_closeButton->setToolTip(tr(
"Stop the tour"));
210 m_prevButton->setText(tr(
"← Back"));
212 m_callout->adjustSize();
223void TourOverlay::repositionCallout()
226 const int cw = m_callout->width();
227 const int ch = m_callout->height();
228 const int ow = width();
229 const int oh = height();
231 if (m_highlightRect.isEmpty()) {
233 m_callout->move((ow - cw) / 2, (oh - ch) / 2);
241 int cx = qBound(pad, spot.left() + (spot.width() - cw) / 2, ow - cw - pad);
242 int cy = spot.bottom() + pad;
244 if (cy + ch + pad > oh) {
246 cy = spot.top() - ch - pad;
252 cx = qMin(spot.right() + pad, ow - cw - pad);
253 if (cx + cw + pad > ow) {
254 cx = qMax(pad, spot.left() - cw - pad);
258 m_callout->move(cx, qBound(pad, cy, oh - ch - pad));
267 if (m_resolver && !m_currentTarget.isEmpty() && m_currentTarget !=
"none") {
268 const QRect liveRect = m_resolver(m_currentTarget);
269 if (liveRect != m_highlightRect) {
270 m_highlightRect = liveRect;
276 p.setRenderHint(QPainter::Antialiasing);
278 if (m_highlightRect.isEmpty()) {
279 p.fillRect(rect(), m_dimColor);
287 full.addRect(rect());
289 hole.addRoundedRect(spot, 6, 6);
290 p.fillPath(full.subtracted(hole), m_dimColor);
292 p.setBrush(Qt::NoBrush);
293 p.setPen(QPen(m_spotlightColor, 2));
294 p.drawRoundedRect(spot, 6, 6);
Theme management types and singleton ThemeManager.
static constexpr int kCalloutMargin
static constexpr int kSpotlightPad
static constexpr int kCalloutWidth
static ThemeManager & instance()
Returns the singleton ThemeManager instance.
void themeChanged()
Emitted whenever the active theme changes.
static Theme effectiveTheme()
Returns the effective theme (Light or Dark), resolving System to the OS preference.
void setTargetResolver(TargetResolver resolver)
TourOverlay(TourEngine *engine, QWidget *parent=nullptr)
void paintEvent(QPaintEvent *event) override
void onStepChanged(int step, int total, const TourStep &stepData)
void setParentWindow(QWidget *newParent)
Re-parents the overlay to newParent and reinstalls the resize event filter.
std::function< QRect(const QString &targetId)> TargetResolver
bool eventFilter(QObject *obj, QEvent *event) override