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(
129 " background: rgba(20,20,20,240);"
130 " border: 1px solid rgba(255,200,50,120);"
131 " border-radius: 8px;"
133 "QLabel { background: transparent; border: none; }"
134 "QPushButton { border-radius: 4px; padding: 4px 12px; font-size: %1px; }")
137 m_stepCounter->setStyleSheet(QString(
"color: rgba(255,200,50,200); font-size: %1px;").arg(counterFontPx));
138 m_titleLabel->setStyleSheet(QString(
"color: white; font-size: %1px; font-weight: bold;").arg(titleFontPx));
139 m_bodyLabel->setStyleSheet(QString(
"color: rgba(220,220,220,230); font-size: %1px;").arg(bodyFontPx));
141 const QString navStyle =
142 QString(
"QPushButton { color: white; background: rgba(255,255,255,40);"
143 " border: 1px solid rgba(255,255,255,80); border-radius: 4px;"
144 " padding: 4px 12px; font-size: %1px; }"
145 "QPushButton:hover { background: rgba(255,255,255,60); }"
146 "QPushButton:disabled { color: rgba(255,255,255,80); background: rgba(80,80,80,60); }")
148 m_closeButton->setStyleSheet(navStyle);
149 m_prevButton->setStyleSheet(navStyle);
150 m_nextButton->setStyleSheet(
151 QString(
"QPushButton { color: white; background: rgba(255,160,20,200);"
152 " border: 1px solid rgba(255,200,50,150); border-radius: 4px;"
153 " padding: 4px 12px; font-size: %1px; }"
154 "QPushButton:hover { background: rgba(255,180,40,230); }")
157 m_dimColor = QColor(0, 0, 0, 90);
158 m_spotlightColor = QColor(0, 120, 210, 220);
160 m_callout->setStyleSheet(
162 " background: rgba(255,255,255,240);"
163 " border: 1px solid rgba(0,100,200,150);"
164 " border-radius: 8px;"
166 "QLabel { background: transparent; border: none; }"
167 "QPushButton { border-radius: 4px; padding: 4px 12px; font-size: %1px; }")
170 m_stepCounter->setStyleSheet(QString(
"color: rgba(0,100,200,200); font-size: %1px;").arg(counterFontPx));
171 m_titleLabel->setStyleSheet(QString(
"color: rgb(20,20,20); font-size: %1px; font-weight: bold;").arg(titleFontPx));
172 m_bodyLabel->setStyleSheet(QString(
"color: rgba(50,50,50,230); font-size: %1px;").arg(bodyFontPx));
174 const QString navStyle =
175 QString(
"QPushButton { color: rgb(20,20,20); background: rgba(0,0,0,20);"
176 " border: 1px solid rgba(0,0,0,80); border-radius: 4px;"
177 " padding: 4px 12px; font-size: %1px; }"
178 "QPushButton:hover { background: rgba(0,0,0,40); }"
179 "QPushButton:disabled { color: rgba(0,0,0,60); background: rgba(0,0,0,10); }")
181 m_closeButton->setStyleSheet(navStyle);
182 m_prevButton->setStyleSheet(navStyle);
183 m_nextButton->setStyleSheet(
184 QString(
"QPushButton { color: white; background: rgba(0,120,210,200);"
185 " border: 1px solid rgba(0,100,200,150); border-radius: 4px;"
186 " padding: 4px 12px; font-size: %1px; }"
187 "QPushButton:hover { background: rgba(0,140,230,230); }")
196 if (obj == parentWidget() && event->type() == QEvent::Resize) {
197 resize(parentWidget()->size());
200 return QWidget::eventFilter(obj, event);
207 m_stepCounter->setText(tr(
"Step %1 of %2").arg(step + 1).arg(total));
208 m_titleLabel->setText(stepData.
title);
209 m_bodyLabel->setText(stepData.
body);
211 m_prevButton->setEnabled(step > 0);
212 m_nextButton->setText(step == total - 1 ? tr(
"Finish") : tr(
"Next →"));
214 m_currentTarget = stepData.
target;
215 m_highlightRect = QRect{};
216 if (m_resolver && !stepData.
target.isEmpty() && stepData.
target !=
"none") {
217 m_highlightRect = m_resolver(stepData.
target);
220 m_callout->adjustSize();
231 const TourStep &stepData = m_engine->currentStepData();
232 const int step = m_engine->currentStep();
233 const int total = m_engine->totalSteps();
235 m_stepCounter->setText(tr(
"Step %1 of %2").arg(step + 1).arg(total));
236 m_titleLabel->setText(stepData.
title);
237 m_bodyLabel->setText(stepData.
body);
238 m_nextButton->setText(step == total - 1 ? tr(
"Finish") : tr(
"Next →"));
240 m_closeButton->setText(tr(
"Exit"));
241 m_closeButton->setToolTip(tr(
"Stop the tour"));
242 m_prevButton->setText(tr(
"← Back"));
244 m_callout->adjustSize();
255void TourOverlay::repositionCallout()
258 const int cw = m_callout->width();
259 const int ch = m_callout->height();
260 const int ow = width();
261 const int oh = height();
263 if (m_highlightRect.isEmpty()) {
265 m_callout->move((ow - cw) / 2, (oh - ch) / 2);
273 int cx = qBound(pad, spot.left() + (spot.width() - cw) / 2, ow - cw - pad);
274 int cy = spot.bottom() + pad;
276 if (cy + ch + pad > oh) {
278 cy = spot.top() - ch - pad;
284 cx = qMin(spot.right() + pad, ow - cw - pad);
285 if (cx + cw + pad > ow) {
286 cx = qMax(pad, spot.left() - cw - pad);
290 m_callout->move(cx, qBound(pad, cy, oh - ch - pad));
299 if (m_resolver && !m_currentTarget.isEmpty() && m_currentTarget !=
"none") {
300 const QRect liveRect = m_resolver(m_currentTarget);
301 if (liveRect != m_highlightRect) {
302 m_highlightRect = liveRect;
308 p.setRenderHint(QPainter::Antialiasing);
310 if (m_highlightRect.isEmpty()) {
311 p.fillRect(rect(), m_dimColor);
319 full.addRect(rect());
321 hole.addRoundedRect(spot, 6, 6);
322 p.fillPath(full.subtracted(hole), m_dimColor);
324 p.setBrush(Qt::NoBrush);
325 p.setPen(QPen(m_spotlightColor, 2));
326 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