FlightGear next
GettingStartedTipsController.cxx
Go to the documentation of this file.
2
3#include <algorithm>
4
5#include <QSettings>
6#include <QDebug>
7#include <QQmlContext>
8#include <QTimer>
9
10#include <QtQml> // qmlContext
11
12#include "GettingStartedTip.hxx"
13#include "TipBackgroundBox.hxx"
14
16{
17 TipGeometryByArrowLocation(GettingStartedTip::Arrow a, const QRectF& g, Qt::Alignment al) :
18 arrow(a),
19 geometry(g),
21 {
22 }
23
25 QRectF geometry;
26 // specify how vertical space is adjusted; is the top, bottom or center fixed
27 Qt::Alignment verticalAlignment = Qt::AlignVCenter;
28};
29
30const double tipBoxWidth = 300.0;
31const double halfBoxWidth = tipBoxWidth * 0.5;
34const double dummyHeight = 200.0;
36
37static std::initializer_list<TipGeometryByArrowLocation> static_tipGeometries = {
43 {GettingStartedTip::Arrow::LeftCenter, QRectF{0.0, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignVCenter},
46 {GettingStartedTip::Arrow::NoArrow, QRectF{-halfBoxWidth, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignVCenter},
47
48};
49
62{
63 Q_OBJECT
64public:
65 ItemPositionObserver(QObject* pr) :
66 QObject(pr)
67 {
68 _notMovingTimeout = new QTimer(this);
69 _notMovingTimeout->setSingleShot(true);
70 _notMovingTimeout->setInterval(1000);
71
72 connect(this, SIGNAL(itemPositionChanged()),
73 _notMovingTimeout, SLOT(start()));
74
75 connect(_notMovingTimeout, &QTimer::timeout,
77 }
78
79 void setObservedItem(QQuickItem* obs)
80 {
81 if (obs == _observedItem)
82 return;
83
84 if (_observedItem) {
85 for (auto o = _observedItem; o; o = o->parentItem()) {
86 disconnect(o, nullptr, this, nullptr);
87 }
88 }
89
90 _observedItem = obs;
91 if (obs) {
92 startObserving(_observedItem);
93 }
94 }
95
96 bool hasRecentlyMoved() const
97 {
98 return _notMovingTimeout->isActive();
99 }
100signals:
102
104private:
105 void startObserving(QQuickItem* obs)
106 {
107
108 connect(obs, &QQuickItem::xChanged, this, &ItemPositionObserver::itemPositionChanged);
109 connect(obs, &QQuickItem::yChanged, this, &ItemPositionObserver::itemPositionChanged);
110 connect(obs, &QQuickItem::widthChanged, this, &ItemPositionObserver::itemPositionChanged);
111 connect(obs, &QQuickItem::heightChanged, this, &ItemPositionObserver::itemPositionChanged);
112
113 // recurse up the item hierarchy
114 if (obs->parentItem()) {
115 startObserving(obs->parentItem());
116 }
117 }
118
119 QPointer<QQuickItem> _observedItem;
120 QTimer* _notMovingTimeout = nullptr;
121};
122
123// static used to ensure only one controller is active at a time
124static QPointer<GettingStartedTipsController> static_activeController;
125
127{
128 // observer for the tip item
129 _positionObserver = new ItemPositionObserver(this);
130 connect(_positionObserver, &ItemPositionObserver::itemPositionChanged,
132
133 // observer for the visual area (which could also be scrolled)
134 _viewAreaObserver = new ItemPositionObserver(this);
135 connect(_viewAreaObserver, &ItemPositionObserver::itemPositionChanged,
137
138 connect(_positionObserver, &ItemPositionObserver::itemNotMoving,
140 connect(_viewAreaObserver, &ItemPositionObserver::itemNotMoving,
142
143 auto qqParent = qobject_cast<QQuickItem*>(parent);
144 if (qqParent) {
145 setVisualArea(qqParent);
146 }
147}
148
152
154{
155 if (_oneShotTip) {
156 return 1;
157 }
158
159 return _tips.size();
160}
161
163{
164 if (_oneShotTip) {
165 return 0;
166 }
167
168 return _index;
169}
170
172{
173 if (_oneShotTip) {
174 return _oneShotTip;
175 }
176
177 if (_tips.empty())
178 return nullptr;
179
180 if ((_index < 0) || (_index >= _tips.size()))
181 return nullptr;
182
183 return _tips.at(_index);
184}
185
187{
188 if (_visualArea == visualArea)
189 return;
190
191 _visualArea = visualArea;
192 _viewAreaObserver->setObservedItem(_visualArea);
193
195 emit visualAreaChanged(_visualArea);
196}
197
199{
200 if (_activeTipHeight == activeTipHeight)
201 return;
202
203 _activeTipHeight = activeTipHeight;
204 emit activeTipHeightChanged(_activeTipHeight);
205 emit tipGeometryChanged();
206}
207
209{
210 if (_scopeActive) {
211 return;
212 }
213
214 if (static_activeController != this) {
215 return;
216 }
217
218 QSettings settings;
219 settings.beginGroup("GettingStarted-DontShow");
220 if (settings.value(tip->tipId()).toBool()) {
221 return;
222 }
223
224 // mark the tip as shown
225 settings.setValue(tip->tipId(), true);
226 _oneShotTip = tip;
227
228 connect(_oneShotTip, &QObject::destroyed, this, &GettingStartedTipsController::onOneShotDestroyed);
229
230 currentTipUpdated();
231 emit indexChanged(0);
232 emit countChanged(count());
233}
234
236{
237 bool a = shouldShowScope();
238 if (a != _scopeActive) {
239 _scopeActive = a;
240 static_activeController = this; // we became active
241 emit activeChanged();
242 currentTipUpdated();
243 }
244}
245
246void GettingStartedTipsController::currentTipUpdated()
247{
248 _positionObserver->setObservedItem(tip());
249
250 emit activeChanged();
251 emit tipChanged();
253 emit tipGeometryChanged();
254}
255
256bool GettingStartedTipsController::addTip(GettingStartedTip *t)
257{
258 if (_tips.contains(t)) {
259 qWarning() << Q_FUNC_INFO << "Duplicate tip" << t;
260 return false;
261 }
262
263 // this logic is important to suppress duplicate tips inside a ListView or Repeater;
264 // effectively, we only show a tip on the first registered instance.
265 Q_FOREACH(GettingStartedTip* tip, _tips) {
266 if (tip->tipId() == t->tipId()) {
267 return false;
268 }
269 }
270
271 _tips.append(t);
272 // order tips by nextTip ID, if defined
273 std::sort(_tips.begin(), _tips.end(), [](const GettingStartedTip* a, GettingStartedTip *b) {
274 return a->nextTip() == b->tipId();
275 });
276
277 currentTipUpdated();
278 emit countChanged(count());
279 return true;
280}
281
282void GettingStartedTipsController::removeTip(GettingStartedTip *t)
283{
284 const bool removedActive = (tip() == t);
285 if (!_tips.removeOne(t)) {
286 qWarning() << Q_FUNC_INFO << "tip not found";
287 }
288
289 if (removedActive) {
290 _index = qMax(_index - 1, 0);
291 }
292
293 currentTipUpdated();
294 emit countChanged(count());
295}
296
297void GettingStartedTipsController::onOneShotDestroyed()
298{
299 if (_oneShotTip == sender()) {
300 emit activeChanged();
301 currentTipUpdated();
302 }
303}
304
306{
307 if (_oneShotTip)
308 return true;
309
310 return _scopeActive && !_tips.empty();
311}
312
314{
315 auto t = tip();
316 if (!_visualArea || !t) {
317 return {};
318 }
319
320 return _visualArea->mapFromItem(t, QPointF{0,0});
321}
322
324{
325 auto t = tip();
326 if (!t)
327 return {};
328
329 const auto arrow = t->arrow();
330 auto it = std::find_if(static_tipGeometries.begin(), static_tipGeometries.end(),
331 [arrow](const TipGeometryByArrowLocation& tg)
332 {
333 return tg.arrow == arrow;
334 });
335
336 if (it == static_tipGeometries.end()) {
337 qWarning() << Q_FUNC_INFO << "Missing tip geometry" << arrow;
338 return {};
339 }
340
341 QRectF g = it->geometry;
346 {
347 g.setHeight(_activeTipHeight);
348 } else {
349 g.setHeight(_activeTipHeight + TipBackgroundBox::arrowHeight());
350 }
351
352 switch (it->verticalAlignment) {
353 case Qt::AlignBottom:
354 g.moveBottom(0);
355 break;
356
357 case Qt::AlignTop:
358 g.moveTop(0);
359 break;
360
361 case Qt::AlignVCenter:
362 g.moveTop(_activeTipHeight * -0.5);
363 break;
364 }
365
366 return g;
367}
368
370{
371 if (!_visualArea || !isActive())
372 return false;
373
374 // hide tips when resizing the window or scrolling; it's visually distracting otherwise
375 if (_positionObserver->hasRecentlyMoved() || _viewAreaObserver->hasRecentlyMoved()) {
376 return false;
377 }
378
379 if (_oneShotTip)
380 return true;
381
382 return !_tips.empty();
383}
384
386{
387 return _activeTipHeight;
388}
389
391{
392 QRectF g(0.0, 0.0, tipBoxWidth, 200.0);
393
394 auto t = tip();
395 if (!t)
396 return g;
397
398 const auto arrow = t->arrow();
402 {
404 }
405
408 }
409
412 g.moveLeft(TipBackgroundBox::arrowHeight());
413 }
414
415 return g;
416}
417
419{
420 // one-shot tips handle this logic differently; we set the don't show
421 // when the tip first appears
422 if (_oneShotTip) {
423 disconnect(_oneShotTip, nullptr, this, nullptr);
424 _oneShotTip = nullptr;
426 } else {
427 if (_scopeActive) {
429 }
430
431 QSettings settings;
432 settings.beginGroup("GettingStarted-DontShow");
433 settings.setValue(_scopeId, true);
434 _scopeActive = false;
435 }
436
437 emit activeChanged();
438 currentTipUpdated();
439}
440
442{
443 if (_oneShotTip) {
444 return;
445 }
446
447 if (_index == index)
448 return;
449
450 _index = qBound(0, index, _tips.size() - 1);
451 _positionObserver->setObservedItem(tip());
452
453 emit indexChanged(_index);
454 currentTipUpdated();
455}
456
458{
459 if (_scopeId == scopeId)
460 return;
461
462 _scopeId = scopeId;
463 _scopeActive = shouldShowScope();
464 if (_scopeActive) {
466 }
467 emit scopeIdChanged(_scopeId);
468 emit activeChanged();
469}
470
471bool GettingStartedTipsController::shouldShowScope() const
472{
474 return false;
475 }
476
477 if (_scopeId.isEmpty())
478 return true;
479
480 QSettings settings;
481 settings.beginGroup("GettingStarted-DontShow");
482 return settings.value(_scopeId).toBool() == false;
483}
484
485
486#include "GettingStartedTipsController.moc"
const double topHeightOffset
const double dummyHeight
static std::initializer_list< TipGeometryByArrowLocation > static_tipGeometries
const double tipBoxWidth
const double arrowSideOffset
const double halfBoxWidth
const double rightSideOffset
static QPointer< GettingStartedTipsController > static_activeController
The GettingStartedTipsController::ItemPositionObserver class.
void indexChanged(int index)
GettingStartedTipsController(QObject *parent=nullptr)
void activeTipHeightChanged(int activeTipHeight)
void showOneShotTip(GettingStartedTip *tip)
showOneShotTip - show a single tip on its own, if it has not previously been shown before.
void scopeIdChanged(QString scopeId)
void countChanged(int count)
void visualAreaChanged(QQuickItem *visualArea)
void setVisualArea(QQuickItem *visualArea)
static int arrowSideOffset()
TipGeometryByArrowLocation(GettingStartedTip::Arrow a, const QRectF &g, Qt::Alignment al)