1
0
Fork 0
flightgear/src/GUI/GettingStartedTipsController.cxx
2021-02-22 10:37:52 +00:00

483 lines
13 KiB
C++
Executable file

#include "GettingStartedTipsController.hxx"
#include <algorithm>
#include <QSettings>
#include <QDebug>
#include <QQmlContext>
#include <QTimer>
#include <QtQml> // qmlContext
#include "GettingStartedTip.hxx"
#include "TipBackgroundBox.hxx"
struct TipGeometryByArrowLocation
{
TipGeometryByArrowLocation(GettingStartedTip::Arrow a, const QRectF& g, Qt::Alignment al) :
arrow(a),
geometry(g),
verticalAlignment(al)
{
}
GettingStartedTip::Arrow arrow;
QRectF geometry;
// specify how vertical space is adjusted; is the top, bottom or center fixed
Qt::Alignment verticalAlignment = Qt::AlignVCenter;
};
const double tipBoxWidth = 300.0;
const double halfBoxWidth = tipBoxWidth * 0.5;
const double arrowSideOffset = TipBackgroundBox::arrowSideOffset();
const double rightSideOffset = -tipBoxWidth + arrowSideOffset;
const double dummyHeight = 200.0;
const double topHeightOffset = -TipBackgroundBox::arrowHeight();
static std::initializer_list<TipGeometryByArrowLocation> static_tipGeometries = {
{GettingStartedTip::Arrow::BottomRight, QRectF{rightSideOffset, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignBottom},
{GettingStartedTip::Arrow::BottomCenter, QRectF{halfBoxWidth, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignBottom},
{GettingStartedTip::Arrow::TopCenter, QRectF{-halfBoxWidth, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignTop},
{GettingStartedTip::Arrow::TopRight, QRectF{rightSideOffset, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignTop},
{GettingStartedTip::Arrow::TopLeft, QRectF{-arrowSideOffset, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignTop},
{GettingStartedTip::Arrow::LeftCenter, QRectF{0.0, 0.0, tipBoxWidth, dummyHeight}, Qt::AlignVCenter},
{GettingStartedTip::Arrow::RightCenter, QRectF{-(tipBoxWidth + TipBackgroundBox::arrowHeight()), 0.0, tipBoxWidth, dummyHeight}, Qt::AlignVCenter},
{GettingStartedTip::Arrow::LeftTop, QRectF{0.0, topHeightOffset, tipBoxWidth, dummyHeight}, Qt::AlignTop},
};
/**
* @brief The GettingStartedTipsController::ItemPositionObserver class
*
* This is a helper to observe the full position (and in the future, transform if required)
* of a QQuickItem, so we can update a signal when the on-screen position changes. This
* is necessary to re-transform the tooltip location if the item it's 'attached' to
* moves, or some ancestor does.
*
* At present this does not handle arbitrary scaling or rotation, but observing those
* signals would also be possible.
*/
class GettingStartedTipsController::ItemPositionObserver : public QObject
{
Q_OBJECT
public:
ItemPositionObserver(QObject* pr) :
QObject(pr)
{
_notMovingTimeout = new QTimer(this);
_notMovingTimeout->setSingleShot(true);
_notMovingTimeout->setInterval(1000);
connect(this, SIGNAL(itemPositionChanged()),
_notMovingTimeout, SLOT(start()));
connect(_notMovingTimeout, &QTimer::timeout,
this, &ItemPositionObserver::itemNotMoving);
}
void setObservedItem(QQuickItem* obs)
{
if (obs == _observedItem)
return;
if (_observedItem) {
for (auto o = _observedItem; o; o = o->parentItem()) {
disconnect(o, nullptr, this, nullptr);
}
}
_observedItem = obs;
if (obs) {
startObserving(_observedItem);
}
}
bool hasRecentlyMoved() const
{
return _notMovingTimeout->isActive();
}
signals:
void itemPositionChanged();
void itemNotMoving();
private:
void startObserving(QQuickItem* obs)
{
connect(obs, &QQuickItem::xChanged, this, &ItemPositionObserver::itemPositionChanged);
connect(obs, &QQuickItem::yChanged, this, &ItemPositionObserver::itemPositionChanged);
connect(obs, &QQuickItem::widthChanged, this, &ItemPositionObserver::itemPositionChanged);
connect(obs, &QQuickItem::heightChanged, this, &ItemPositionObserver::itemPositionChanged);
// recurse up the item hierarchy
if (obs->parentItem()) {
startObserving(obs->parentItem());
}
}
QPointer<QQuickItem> _observedItem;
QTimer* _notMovingTimeout = nullptr;
};
// static used to ensure only one controller is active at a time
static QPointer<GettingStartedTipsController> static_activeController;
GettingStartedTipsController::GettingStartedTipsController(QObject *parent) : QObject(parent)
{
// observer for the tip item
_positionObserver = new ItemPositionObserver(this);
connect(_positionObserver, &ItemPositionObserver::itemPositionChanged,
this, &GettingStartedTipsController::tipPositionInVisualAreaChanged);
// observer for the visual area (which could also be scrolled)
_viewAreaObserver = new ItemPositionObserver(this);
connect(_viewAreaObserver, &ItemPositionObserver::itemPositionChanged,
this, &GettingStartedTipsController::tipPositionInVisualAreaChanged);
connect(_positionObserver, &ItemPositionObserver::itemNotMoving,
this, &GettingStartedTipsController::tipPositionInVisualAreaChanged);
connect(_viewAreaObserver, &ItemPositionObserver::itemNotMoving,
this, &GettingStartedTipsController::tipPositionInVisualAreaChanged);
auto qqParent = qobject_cast<QQuickItem*>(parent);
if (qqParent) {
setVisualArea(qqParent);
}
}
GettingStartedTipsController::~GettingStartedTipsController()
{
}
int GettingStartedTipsController::count() const
{
if (_oneShotTip) {
return 1;
}
return _tips.size();
}
int GettingStartedTipsController::index() const
{
if (_oneShotTip) {
return 0;
}
return _index;
}
GettingStartedTip *GettingStartedTipsController::tip() const
{
if (_oneShotTip) {
return _oneShotTip;
}
if (_tips.empty())
return nullptr;
if ((_index < 0) || (_index >= _tips.size()))
return nullptr;
return _tips.at(_index);
}
void GettingStartedTipsController::setVisualArea(QQuickItem *visualArea)
{
if (_visualArea == visualArea)
return;
_visualArea = visualArea;
_viewAreaObserver->setObservedItem(_visualArea);
emit tipPositionInVisualAreaChanged();
emit visualAreaChanged(_visualArea);
}
void GettingStartedTipsController::setActiveTipHeight(int activeTipHeight)
{
if (_activeTipHeight == activeTipHeight)
return;
_activeTipHeight = activeTipHeight;
emit activeTipHeightChanged(_activeTipHeight);
emit tipGeometryChanged();
}
void GettingStartedTipsController::showOneShotTip(GettingStartedTip *tip)
{
if (_scopeActive) {
return;
}
if (static_activeController != this) {
return;
}
QSettings settings;
settings.beginGroup("GettingStarted-DontShow");
if (settings.value(tip->tipId()).toBool()) {
return;
}
// mark the tip as shown
settings.setValue(tip->tipId(), true);
_oneShotTip = tip;
connect(_oneShotTip, &QObject::destroyed, this, &GettingStartedTipsController::onOneShotDestroyed);
currentTipUpdated();
emit indexChanged(0);
emit countChanged(count());
}
void GettingStartedTipsController::tipsWereReset()
{
bool a = shouldShowScope();
if (a != _scopeActive) {
_scopeActive = a;
static_activeController = this; // we became active
emit activeChanged();
currentTipUpdated();
}
}
void GettingStartedTipsController::currentTipUpdated()
{
_positionObserver->setObservedItem(tip());
emit activeChanged();
emit tipChanged();
emit tipPositionInVisualAreaChanged();
emit tipGeometryChanged();
}
bool GettingStartedTipsController::addTip(GettingStartedTip *t)
{
if (_tips.contains(t)) {
qWarning() << Q_FUNC_INFO << "Duplicate tip" << t;
return false;
}
// this logic is important to suppress duplicate tips inside a ListView or Repeater;
// effectively, we only show a tip on the first registered instance.
Q_FOREACH(GettingStartedTip* tip, _tips) {
if (tip->tipId() == t->tipId()) {
return false;
}
}
_tips.append(t);
// order tips by nextTip ID, if defined
std::sort(_tips.begin(), _tips.end(), [](const GettingStartedTip* a, GettingStartedTip *b) {
return a->nextTip() == b->tipId();
});
currentTipUpdated();
emit countChanged(count());
return true;
}
void GettingStartedTipsController::removeTip(GettingStartedTip *t)
{
const bool removedActive = (tip() == t);
if (!_tips.removeOne(t)) {
qWarning() << Q_FUNC_INFO << "tip not found";
}
if (removedActive) {
_index = qMax(_index - 1, 0);
}
currentTipUpdated();
emit countChanged(count());
}
void GettingStartedTipsController::onOneShotDestroyed()
{
if (_oneShotTip == sender()) {
emit activeChanged();
currentTipUpdated();
}
}
bool GettingStartedTipsController::isActive() const
{
if (_oneShotTip)
return true;
return _scopeActive && !_tips.empty();
}
QPointF GettingStartedTipsController::tipPositionInVisualArea() const
{
auto t = tip();
if (!_visualArea || !t) {
return {};
}
return _visualArea->mapFromItem(t, QPointF{0,0});
}
QRectF GettingStartedTipsController::tipGeometry() const
{
auto t = tip();
if (!t)
return {};
const auto arrow = t->arrow();
auto it = std::find_if(static_tipGeometries.begin(), static_tipGeometries.end(),
[arrow](const TipGeometryByArrowLocation& tg)
{
return tg.arrow == arrow;
});
if (it == static_tipGeometries.end()) {
qWarning() << Q_FUNC_INFO << "Missing tip geometry" << arrow;
return {};
}
QRectF g = it->geometry;
if ((arrow == GettingStartedTip::Arrow::LeftCenter) || (arrow == GettingStartedTip::Arrow::RightCenter)
|| (arrow == GettingStartedTip::Arrow::LeftTop)) {
g.setHeight(_activeTipHeight);
} else {
g.setHeight(_activeTipHeight + TipBackgroundBox::arrowHeight());
}
switch (it->verticalAlignment) {
case Qt::AlignBottom:
g.moveBottom(0);
break;
case Qt::AlignTop:
g.moveTop(0);
break;
case Qt::AlignVCenter:
g.moveTop(_activeTipHeight * -0.5);
break;
}
return g;
}
bool GettingStartedTipsController::tipPositionValid() const
{
if (!_visualArea || !isActive())
return false;
// hide tips when resizing the window or scrolling; it's visually distracting otherwise
if (_positionObserver->hasRecentlyMoved() || _viewAreaObserver->hasRecentlyMoved()) {
return false;
}
if (_oneShotTip)
return true;
return !_tips.empty();
}
int GettingStartedTipsController::activeTipHeight() const
{
return _activeTipHeight;
}
QRectF GettingStartedTipsController::contentGeometry() const
{
QRectF g(0.0, 0.0, tipBoxWidth, 200.0);
auto t = tip();
if (!t)
return g;
const auto arrow = t->arrow();
if ((arrow == GettingStartedTip::Arrow::TopCenter) ||
(arrow == GettingStartedTip::Arrow::TopLeft) ||
(arrow == GettingStartedTip::Arrow::TopRight))
{
g.moveTop(TipBackgroundBox::arrowHeight());
}
if (arrow == GettingStartedTip::Arrow::RightCenter) {
g.setWidth(tipBoxWidth - TipBackgroundBox::arrowHeight());
}
if (arrow == GettingStartedTip::Arrow::LeftCenter) {
g.setWidth(tipBoxWidth - TipBackgroundBox::arrowHeight());
g.moveLeft(TipBackgroundBox::arrowHeight());
}
return g;
}
void GettingStartedTipsController::close()
{
// one-shot tips handle this logic differently; we set the don't show
// when the tip first appears
if (_oneShotTip) {
disconnect(_oneShotTip, nullptr, this, nullptr);
_oneShotTip = nullptr;
static_activeController.clear();
} else {
if (_scopeActive) {
static_activeController.clear();
}
QSettings settings;
settings.beginGroup("GettingStarted-DontShow");
settings.setValue(_scopeId, true);
_scopeActive = false;
}
emit activeChanged();
currentTipUpdated();
}
void GettingStartedTipsController::setIndex(int index)
{
if (_oneShotTip) {
return;
}
if (_index == index)
return;
_index = qBound(0, index, _tips.size() - 1);
_positionObserver->setObservedItem(tip());
emit indexChanged(_index);
currentTipUpdated();
}
void GettingStartedTipsController::setScopeId(QString scopeId)
{
if (_scopeId == scopeId)
return;
_scopeId = scopeId;
_scopeActive = shouldShowScope();
if (_scopeActive) {
static_activeController = this;
}
emit scopeIdChanged(_scopeId);
emit activeChanged();
}
bool GettingStartedTipsController::shouldShowScope() const
{
if (static_activeController && (static_activeController != this)) {
return false;
}
if (_scopeId.isEmpty())
return true;
QSettings settings;
settings.beginGroup("GettingStarted-DontShow");
return settings.value(_scopeId).toBool() == false;
}
#include "GettingStartedTipsController.moc"