From 6b6defbeadd9375ca58e4459da6955b165cf2bb6 Mon Sep 17 00:00:00 2001 From: Automatic Release Builder <build@flightgear.org> Date: Tue, 10 Nov 2020 11:09:46 +0000 Subject: [PATCH] Add getting-started tips to the launcher QML implementation of getting started tips, with a nice styled background box. Tips are defined+positioned inline, and displayed when their enclosing scope is active. --- src/GUI/CMakeLists.txt | 8 + src/GUI/GettingStartedScope.cxx | 29 ++ src/GUI/GettingStartedScope.hxx | 43 ++ src/GUI/GettingStartedTip.cxx | 147 ++++++ src/GUI/GettingStartedTip.hxx | 110 +++++ src/GUI/GettingStartedTipsController.cxx | 463 ++++++++++++++++++ src/GUI/GettingStartedTipsController.hxx | 150 ++++++ src/GUI/LauncherController.cxx | 21 + src/GUI/LauncherController.hxx | 3 + src/GUI/LauncherMainWindow.cxx | 1 + src/GUI/QtLauncher.cxx | 3 + src/GUI/TipBackgroundBox.cxx | 192 ++++++++ src/GUI/TipBackgroundBox.hxx | 61 +++ src/GUI/qml/AircraftCompactDelegate.qml | 19 + src/GUI/qml/AircraftDetailsView.qml | 23 +- src/GUI/qml/AircraftList.qml | 60 +++ src/GUI/qml/AircraftPreviewPanel.qml | 13 +- src/GUI/qml/AircraftRatingsPanel.qml | 21 + .../DidMigrateOfficialCatalogNotification.qml | 2 +- src/GUI/qml/GettingStartedTipDisplay.qml | 134 +++++ src/GUI/qml/GettingStartedTipLayer.qml | 45 ++ src/GUI/qml/HelpSupport.qml | 15 + src/GUI/qml/ListHeaderBox.qml | 1 + src/GUI/qml/Section.qml | 14 +- src/GUI/qml/Settings.qml | 21 + src/GUI/qml/Summary.qml | 64 +++ src/GUI/resources.qrc | 2 + 27 files changed, 1660 insertions(+), 5 deletions(-) create mode 100644 src/GUI/GettingStartedScope.cxx create mode 100644 src/GUI/GettingStartedScope.hxx create mode 100644 src/GUI/GettingStartedTip.cxx create mode 100644 src/GUI/GettingStartedTip.hxx create mode 100755 src/GUI/GettingStartedTipsController.cxx create mode 100644 src/GUI/GettingStartedTipsController.hxx create mode 100644 src/GUI/TipBackgroundBox.cxx create mode 100644 src/GUI/TipBackgroundBox.hxx create mode 100644 src/GUI/qml/GettingStartedTipDisplay.qml create mode 100644 src/GUI/qml/GettingStartedTipLayer.qml diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt index adbd667b8..f31fbf604 100644 --- a/src/GUI/CMakeLists.txt +++ b/src/GUI/CMakeLists.txt @@ -201,6 +201,14 @@ if (HAVE_QT) PathUrlHelper.hxx DialogStateController.cxx DialogStateController.hxx + GettingStartedTip.hxx + GettingStartedTip.cxx + GettingStartedTipsController.cxx + GettingStartedTipsController.hxx + TipBackgroundBox.cxx + TipBackgroundBox.hxx + GettingStartedScope.hxx + GettingStartedScope.cxx ${QQUI_SOURCES} ) diff --git a/src/GUI/GettingStartedScope.cxx b/src/GUI/GettingStartedScope.cxx new file mode 100644 index 000000000..99ae3eb73 --- /dev/null +++ b/src/GUI/GettingStartedScope.cxx @@ -0,0 +1,29 @@ +#include "GettingStartedScope.hxx" + +#include "GettingStartedTipsController.hxx" + + +GettingStartedScope::GettingStartedScope(QObject *parent) : QObject(parent) +{ + +} + +GettingStartedScopeAttached *GettingStartedScope::qmlAttachedProperties(QObject *object) +{ + auto c = new GettingStartedScopeAttached(object); + return c; +} + +GettingStartedScopeAttached::GettingStartedScopeAttached(QObject *parent) : QObject(parent) +{ + +} + +void GettingStartedScopeAttached::setController(GettingStartedTipsController *controller) +{ + if (_controller == controller) + return; + + _controller = controller; + emit controllerChanged(); +} diff --git a/src/GUI/GettingStartedScope.hxx b/src/GUI/GettingStartedScope.hxx new file mode 100644 index 000000000..c3abeb793 --- /dev/null +++ b/src/GUI/GettingStartedScope.hxx @@ -0,0 +1,43 @@ +#pragma once + +#include <QObject> +#include <QQmlEngine> + +class GettingStartedTipsController; + +class GettingStartedScopeAttached : public QObject +{ + Q_OBJECT + + Q_PROPERTY(GettingStartedTipsController* controller READ controller WRITE setController NOTIFY controllerChanged) +public: + GettingStartedScopeAttached(QObject* parent); + + GettingStartedTipsController* controller() const + { + return _controller; + } + +public slots: + void setController(GettingStartedTipsController* controller); + +signals: + void controllerChanged(); + +private: + GettingStartedTipsController* _controller = nullptr; +}; + +class GettingStartedScope : public QObject +{ + Q_OBJECT + +public: + explicit GettingStartedScope(QObject *parent = nullptr); + + static GettingStartedScopeAttached* qmlAttachedProperties(QObject *object); +signals: + +}; + +QML_DECLARE_TYPEINFO(GettingStartedScope, QML_HAS_ATTACHED_PROPERTIES) diff --git a/src/GUI/GettingStartedTip.cxx b/src/GUI/GettingStartedTip.cxx new file mode 100644 index 000000000..153fa876e --- /dev/null +++ b/src/GUI/GettingStartedTip.cxx @@ -0,0 +1,147 @@ +#include "GettingStartedTip.hxx" + +#include "GettingStartedScope.hxx" +#include "GettingStartedTipsController.hxx" + +static bool static_globalEnableTips = true; + +GettingStartedTip::GettingStartedTip(QQuickItem *parent) : + QQuickItem(parent) +{ + _enabled = static_globalEnableTips; + + setImplicitHeight(1); + setImplicitWidth(1); + + if (_enabled) { + registerWithScope(); + } +} + +GettingStartedTip::~GettingStartedTip() +{ + if (_enabled) { + unregisterFromScope(); + } +} + +GettingStartedTipsController *GettingStartedTip::controller() const +{ + return _controller.data(); +} + +void GettingStartedTip::componentComplete() +{ + if (_enabled && !_controller) { + registerWithScope(); + } + + QQuickItem::componentComplete(); +} + +void GettingStartedTip::setGlobalTipsEnabled(bool enable) +{ + static_globalEnableTips = enable; +} + +void GettingStartedTip::setEnabled(bool enabled) +{ + if (_enabled == enabled) + return; + + _enabled = enabled & static_globalEnableTips; + if (enabled) { + registerWithScope(); + } else { + unregisterFromScope(); + } + + emit enabledChanged(); +} + +void GettingStartedTip::showOneShot() +{ + auto ctl = findController(); + if (ctl) { + ctl->showOneShotTip(this); + } +} + +void GettingStartedTip::setStandalone(bool standalone) +{ + if (_standalone == standalone) + return; + + _standalone = standalone; + emit standaloneChanged(_standalone); + + // re-register with our scope, which will also unregister us if + // necessary. + if (_enabled) { + registerWithScope(); + } +} + +void GettingStartedTip::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + const bool isParentChanged = (change == ItemParentHasChanged); + if (isParentChanged && _enabled) { + unregisterFromScope(); + } + + QQuickItem::itemChange(change, value); + + if (isParentChanged && _enabled) { + registerWithScope(); + } +} + +void GettingStartedTip::registerWithScope() +{ + if (_controller) { + unregisterFromScope(); + } + + if (_standalone) { + return; // standalone tips don't register + } + + auto ctl = findController(); + if (!ctl) { + return; + } + + bool ok = ctl->addTip(this); + if (ok) { + _controller = ctl; + emit controllerChanged(); + } +} + +GettingStartedTipsController* GettingStartedTip::findController() +{ + QQuickItem* pr = const_cast<QQuickItem*>(parentItem()); + if (!pr) { + return nullptr; + } + + while (pr) { + auto sa = qobject_cast<GettingStartedScopeAttached*>(qmlAttachedPropertiesObject<GettingStartedScope>(pr, false)); + if (sa && sa->controller()) { + return sa->controller(); + } + + pr = pr->parentItem(); + } + + return nullptr; +} + +void GettingStartedTip::unregisterFromScope() +{ + if (_controller) { + _controller->removeTip(this); + _controller.clear(); + emit controllerChanged(); + } +} diff --git a/src/GUI/GettingStartedTip.hxx b/src/GUI/GettingStartedTip.hxx new file mode 100644 index 000000000..4ea540b59 --- /dev/null +++ b/src/GUI/GettingStartedTip.hxx @@ -0,0 +1,110 @@ +#ifndef GETTINGSTARTEDTIP_HXX +#define GETTINGSTARTEDTIP_HXX + +#include <QQuickItem> +#include <QString> +#include <QPointer> + +#include "GettingStartedTipsController.hxx" + +class GettingStartedTip : public QQuickItem +{ + Q_OBJECT + + Q_PROPERTY(QString tipId MEMBER _id NOTIFY tipChanged) + Q_PROPERTY(QString text MEMBER _text NOTIFY tipChanged) + Q_PROPERTY(QString nextTip MEMBER _nextId NOTIFY tipChanged) + + Q_PROPERTY(Arrow arrow MEMBER _arrow NOTIFY tipChanged) + + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + + Q_PROPERTY(GettingStartedTipsController* controller READ controller NOTIFY controllerChanged) + + /// standalone tips are excluded from their scope when activated; + /// instead they need to be activated manually + Q_PROPERTY(bool standalone READ standalone WRITE setStandalone NOTIFY standaloneChanged) +public: + enum class Arrow + { + TopCenter, // directly below the item, centered + LeftCenter, + RightCenter, + BottomCenter, + BottomRight, + TopRight, + TopLeft, + LeftTop, // on the left side, at the top + }; + + Q_ENUM(Arrow) + + explicit GettingStartedTip(QQuickItem *parent = nullptr); + ~GettingStartedTip() override; + + QString tipId() const + { + return _id; + } + + Arrow arrow() const + { + return _arrow; + } + + bool isEnabled() const + { + return _enabled; + } + + GettingStartedTipsController* controller() const; + + void componentComplete() override; // from QQmlParserStatus + + bool standalone() const + { + return _standalone; + } + + QString nextTip() const + { + return _nextId; + } + + // allow disabling all tips progrmatically : this is a temporary + // measure to make life less annoying for our translators + static void setGlobalTipsEnabled(bool enable); +public slots: + void setEnabled(bool enabled); + + void showOneShot(); + void setStandalone(bool standalone); + +signals: + + void tipChanged(); + void enabledChanged(); + void controllerChanged(); + + void standaloneChanged(bool standalone); + +protected: + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value); + +private: + void registerWithScope(); + void unregisterFromScope(); + + GettingStartedTipsController* findController(); + + QString _id, + _text, + _nextId; + + Arrow _arrow = Arrow::LeftCenter; + bool _enabled = true; + QPointer<GettingStartedTipsController> _controller; + bool _standalone = false; +}; + +#endif // GETTINGSTARTEDTIP_HXX diff --git a/src/GUI/GettingStartedTipsController.cxx b/src/GUI/GettingStartedTipsController.cxx new file mode 100755 index 000000000..e133e4521 --- /dev/null +++ b/src/GUI/GettingStartedTipsController.cxx @@ -0,0 +1,463 @@ +#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; +}; + +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; + } + + 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; + 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; + } else { + 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(); + emit scopeIdChanged(_scopeId); + emit activeChanged(); +} + +bool GettingStartedTipsController::shouldShowScope() const +{ + if (_scopeId.isEmpty()) + return true; + + QSettings settings; + settings.beginGroup("GettingStarted-DontShow"); + return settings.value(_scopeId).toBool() == false; +} + + +#include "GettingStartedTipsController.moc" diff --git a/src/GUI/GettingStartedTipsController.hxx b/src/GUI/GettingStartedTipsController.hxx new file mode 100644 index 000000000..37bcfa8d7 --- /dev/null +++ b/src/GUI/GettingStartedTipsController.hxx @@ -0,0 +1,150 @@ +#ifndef GETTINGSTARTEDTIPSCONTROLLER_HXX +#define GETTINGSTARTEDTIPSCONTROLLER_HXX + +#include <QObject> +#include <QVector> +#include <QQuickItem> +#include <QPointer> + +// forward decls +class GettingStartedTip; + +/** + * @brief Manage presentation of getting started tips on a screen/page + */ +class GettingStartedTipsController : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString scopeId READ scopeId WRITE setScopeId NOTIFY scopeIdChanged) + + Q_PROPERTY(GettingStartedTip* tip READ tip NOTIFY tipChanged) + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged) + + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) + + Q_PROPERTY(QPointF tipPositionInVisualArea READ tipPositionInVisualArea NOTIFY tipPositionInVisualAreaChanged) + Q_PROPERTY(bool tipPositionValid READ tipPositionValid NOTIFY tipPositionInVisualAreaChanged) + + Q_PROPERTY(QRectF tipGeometry READ tipGeometry NOTIFY tipGeometryChanged) + Q_PROPERTY(int activeTipHeight READ activeTipHeight WRITE setActiveTipHeight NOTIFY activeTipHeightChanged) + Q_PROPERTY(QRectF contentGeometry READ contentGeometry NOTIFY tipChanged) + + Q_PROPERTY(QQuickItem* visualArea READ visualArea WRITE setVisualArea NOTIFY visualAreaChanged) + +public: + explicit GettingStartedTipsController(QObject *parent = nullptr); + ~GettingStartedTipsController(); + + int count() const; + + int index() const; + + GettingStartedTip* tip() const; + + QString scopeId() const + { + return _scopeId; + } + + bool isActive() const; + + QPointF tipPositionInVisualArea() const; + + QQuickItem* visualArea() const + { + return _visualArea; + } + + QRectF tipGeometry() const; + + bool tipPositionValid() const; + + int activeTipHeight() const; + + /** + * @brief contentGeometry - based on the active tip, return the box + * (relative to the total tipGeometry) which content should occupy. + * This allows for offseting due to the arrow position, and also specifies + * the width for computing wrapped text height + * + * The actual height is the maximum height; the computed value should be + * set by activeTipHeight + */ + QRectF contentGeometry() const; + +public slots: + + void close(); + + void setIndex(int index); + + void setScopeId(QString scopeId); + + void setVisualArea(QQuickItem* visualArea); + + void setActiveTipHeight(int activeTipHeight); + + /** + * @brief showOneShotTip - show a single tip on its own, if it has not + * previously been shown before. + * + * This is used for pieces of UI which are not always present; the first + * time the UI is displayed to the user, we can show a tip describing it. + * Once the user closes the tip, it will not reappear. + * + * The tip will be activated if the controller is currently inactive. + * If the controller was alreayd active, the tip will be shown, and when + * closed, the other tips will be shown. If the one-shot tip is part of + * the currently active list, this actually does nothing, to avoid showing + * the same tip twice + */ + void showOneShotTip(GettingStartedTip* tip); + + void tipsWereReset(); +signals: + + void countChanged(int count); + void indexChanged(int index); + void tipChanged(); + void scopeChanged(QObject* scope); + void scopeIdChanged(QString scopeId); + void activeChanged(); + void tipGeometryChanged(); + + void tipPositionInVisualAreaChanged(); + + void visualAreaChanged(QQuickItem* visualArea); + + void activeTipHeightChanged(int activeTipHeight); + +private: + friend class GettingStartedTip; + + bool shouldShowScope() const; + + void currentTipUpdated(); + + bool addTip(GettingStartedTip* t); + void removeTip(GettingStartedTip* t); + + void onOneShotDestroyed(); +private: + + class ItemPositionObserver; + + bool _scopeActive = false; + int _index = 0; + QString _scopeId; + ItemPositionObserver* _positionObserver = nullptr; + ItemPositionObserver* _viewAreaObserver = nullptr; + + QPointer<GettingStartedTip> _oneShotTip; + QVector<GettingStartedTip*> _tips; + QQuickItem* _visualArea = nullptr; + int _activeTipHeight = 0; +}; + +#endif // GETTINGSTARTEDTIPSCONTROLLER_HXX diff --git a/src/GUI/LauncherController.cxx b/src/GUI/LauncherController.cxx index a40e181ff..ae7fd93ff 100644 --- a/src/GUI/LauncherController.cxx +++ b/src/GUI/LauncherController.cxx @@ -59,6 +59,10 @@ #include "ThumbnailImageItem.hxx" #include "UnitsModel.hxx" #include "UpdateChecker.hxx" +#include "GettingStartedTipsController.hxx" +#include "GettingStartedTip.hxx" +#include "TipBackgroundBox.hxx" +#include "GettingStartedScope.hxx" using namespace simgear::pkg; @@ -199,6 +203,11 @@ void LauncherController::initQML() qmlRegisterSingletonType(QUrl("qrc:/qml/OverlayShared.qml"), "FlightGear", 1, 0, "OverlayShared"); + qmlRegisterType<GettingStartedScope>("FlightGear", 1, 0, "GettingStartedScope"); + qmlRegisterType<GettingStartedTipsController>("FlightGear", 1, 0, "GettingStartedController"); + qmlRegisterType<GettingStartedTip>("FlightGear", 1, 0, "GettingStartedTip"); + qmlRegisterType<TipBackgroundBox>("FlightGear", 1, 0, "TipBackgroundBox"); + QNetworkDiskCache* diskCache = new QNetworkDiskCache(this); SGPath cachePath = globals->get_fg_home() / "PreviewsCache"; diskCache->setCacheDirectory(QString::fromStdString(cachePath.utf8Str())); @@ -876,6 +885,18 @@ void LauncherController::setAircraftGridMode(bool aircraftGridMode) emit aircraftGridModeChanged(m_aircraftGridMode); } +void LauncherController::resetGettingStartedTips() +{ + { + QSettings settings; + settings.beginGroup("GettingStarted-DontShow"); + settings.remove(""); // remove all keys in the current group + settings.endGroup(); + } // ensure settings are written, before we emit the signal + + emit didResetGettingStartedTips(); +} + void LauncherController::setMinWindowSize(QSize sz) { if (sz == m_minWindowSize) diff --git a/src/GUI/LauncherController.hxx b/src/GUI/LauncherController.hxx index 933de078e..1cc23d2b1 100644 --- a/src/GUI/LauncherController.hxx +++ b/src/GUI/LauncherController.hxx @@ -243,6 +243,7 @@ signals: void inAppChanged(); void installedAircraftCountChanged(int installedAircraftCount); + void didResetGettingStartedTips(); public slots: void setSelectedAircraft(QUrl selectedAircraft); @@ -263,6 +264,8 @@ public slots: void saveConfigAs(); void setAircraftGridMode(bool aircraftGridMode); + void resetGettingStartedTips(); + private slots: void onAircraftInstalledCompleted(QModelIndex index); diff --git a/src/GUI/LauncherMainWindow.cxx b/src/GUI/LauncherMainWindow.cxx index 98fea487b..f9bfe3902 100755 --- a/src/GUI/LauncherMainWindow.cxx +++ b/src/GUI/LauncherMainWindow.cxx @@ -28,6 +28,7 @@ #include "LocationController.hxx" #include "QtLauncher.hxx" #include "UpdateChecker.hxx" +#include "GettingStartedTip.hxx" ////////////////////////////////////////////////////////////////////////////// diff --git a/src/GUI/QtLauncher.cxx b/src/GUI/QtLauncher.cxx index f332749b8..9708fc656 100644 --- a/src/GUI/QtLauncher.cxx +++ b/src/GUI/QtLauncher.cxx @@ -75,6 +75,7 @@ #include "LocalAircraftCache.hxx" #include "PathListModel.hxx" #include "UnitsModel.hxx" +#include "GettingStartedTip.hxx" #if defined(SG_MAC) #include <GUI/CocoaHelpers.h> @@ -110,6 +111,7 @@ void initNavCache() const char* waitForOtherMsg = QT_TRANSLATE_NOOP("initNavCache", "Another copy of FlightGear is creating the navigation database. Waiting for it to finish."); QString m = qApp->translate("initNavCache", waitForOtherMsg); + addSentryBreadcrumb("Launcher: showing wait for other process NavCache rebuild dialog", "info"); QProgressDialog waitForRebuild(m, QString() /* cancel text */, 0, 0, Q_NULLPTR, @@ -133,6 +135,7 @@ void initNavCache() updateTimer.start(); // timer won't actually run until we process events waitForRebuild.exec(); updateTimer.stop(); + addSentryBreadcrumb("Launcher: done waiting for other process NavCache rebuild dialog", "info"); } NavDataCache* cache = NavDataCache::createInstance(); diff --git a/src/GUI/TipBackgroundBox.cxx b/src/GUI/TipBackgroundBox.cxx new file mode 100644 index 000000000..843e382ad --- /dev/null +++ b/src/GUI/TipBackgroundBox.cxx @@ -0,0 +1,192 @@ +#include "TipBackgroundBox.hxx" + +#include <QPainter> +#include <QPainterPath> +#include <QDebug> + +namespace { + +void pathLineBy(QPainterPath& pp, double x, double y) +{ + const auto c = pp.currentPosition(); + pp.lineTo(c.x() + x, c.y() + y); +} + +const double arrowDim1 = 20.0; +const double arrowEndOffset = 30; + +QPainterPath pathFromArrowAndGeometry(GettingStartedTip::Arrow arrow, const QRectF& g) +{ + QPainterPath pp; + + switch (arrow) { + case GettingStartedTip::Arrow::TopCenter: + pp.moveTo(g.center().x(), 0.0); + pp.lineTo(g.center().x() + arrowDim1, arrowDim1); + pp.lineTo(g.right(), arrowDim1); + pp.lineTo(g.right(), g.bottom()); + pp.lineTo(g.left(), g.bottom()); + pp.lineTo(g.left(), arrowDim1); + pp.lineTo(g.center().x() - arrowDim1, arrowDim1); + pp.closeSubpath(); + break; + + case GettingStartedTip::Arrow::BottomRight: + pp.moveTo(g.right() - arrowEndOffset, g.bottom()); + pathLineBy(pp, arrowDim1, -arrowDim1); + pp.lineTo(g.right(), g.bottom() - arrowDim1); + pp.lineTo(g.right(), g.top()); + pp.lineTo(g.left(), g.top()); + pp.lineTo(g.left(), g.bottom() - arrowDim1); + pp.lineTo(g.right() - arrowEndOffset - arrowDim1, g.bottom() - arrowDim1); + pp.closeSubpath(); + break; + + case GettingStartedTip::Arrow::TopRight: + pp.moveTo(g.right() - arrowEndOffset, 0.0); + pathLineBy(pp, arrowDim1, arrowDim1); + pp.lineTo(g.right(), arrowDim1); + pp.lineTo(g.right(), g.bottom()); + pp.lineTo(g.left(), g.bottom()); + pp.lineTo(g.left(), arrowDim1); + pp.lineTo(g.right() - (arrowEndOffset + arrowDim1), arrowDim1); + pp.closeSubpath(); + break; + + case GettingStartedTip::Arrow::TopLeft: + pp.moveTo(arrowEndOffset, 0.0); + pathLineBy(pp, arrowDim1, arrowDim1); + pp.lineTo(g.right(), arrowDim1); + pp.lineTo(g.right(), g.bottom()); + pp.lineTo(g.left(), g.bottom()); + pp.lineTo(g.left(), arrowDim1); + pp.lineTo(arrowEndOffset - arrowDim1, arrowDim1); + pp.closeSubpath(); + break; + + case GettingStartedTip::Arrow::LeftCenter: + pp.moveTo(0.0, g.center().y()); + pathLineBy(pp, arrowDim1, -arrowDim1); + pp.lineTo(arrowDim1, g.top()); + pp.lineTo(g.right(), g.top()); + pp.lineTo(g.right(), g.bottom()); + pp.lineTo(arrowDim1, g.bottom()); + pp.lineTo(arrowDim1, g.center().y() + arrowDim1); + pp.closeSubpath(); + break; + + case GettingStartedTip::Arrow::RightCenter: + pp.moveTo(g.right(), g.center().y()); + pathLineBy(pp, -arrowDim1, arrowDim1); + pp.lineTo(g.right() - arrowDim1, g.bottom()); + pp.lineTo(g.left(), g.bottom()); + pp.lineTo(g.left(), g.top()); + pp.lineTo(g.right() - arrowDim1, g.top()); + pp.lineTo(g.right() - arrowDim1, g.center().y() - arrowDim1); + pp.closeSubpath(); + break; + + + case GettingStartedTip::Arrow::LeftTop: + pp.moveTo(0.0, g.top() + arrowDim1); + pathLineBy(pp, arrowDim1, -arrowDim1); + pp.lineTo(g.right(), g.top()); + pp.lineTo(g.right(), g.bottom()); + pp.lineTo(g.left() + arrowDim1, g.bottom()); + pp.lineTo(g.left() + arrowDim1, -g.left() + arrowDim1); + pp.closeSubpath(); + break; + + default: + qWarning() << Q_FUNC_INFO << "unhandled:" << arrow; + break; + } + + return pp; +} + + +} // of anonymous + +int TipBackgroundBox::arrowSideOffset() +{ + return static_cast<int>(arrowEndOffset); +} + +int TipBackgroundBox::arrowHeight() +{ + return static_cast<int>(arrowDim1); +} + +TipBackgroundBox::TipBackgroundBox(QQuickItem* pr) : + QQuickPaintedItem(pr) +{ +} + +GettingStartedTip::Arrow TipBackgroundBox::arrowPosition() const +{ + return _arrow; +} + +QColor TipBackgroundBox::borderColor() const +{ + return _borderColor; +} + +int TipBackgroundBox::borderWidth() const +{ + return _borderWidth; +} + +QColor TipBackgroundBox::fill() const +{ + return _fill; +} + +void TipBackgroundBox::setArrowPosition(GettingStartedTip::Arrow arrow) +{ + if (_arrow == arrow) + return; + + _arrow = arrow; + emit arrowPositionChanged(_arrow); + update(); +} + +void TipBackgroundBox::setBorderColor(QColor borderColor) +{ + if (_borderColor == borderColor) + return; + + _borderColor = borderColor; + emit borderColorChanged(_borderColor); + update(); +} + +void TipBackgroundBox::setBorderWidth(int borderWidth) +{ + if (_borderWidth == borderWidth) + return; + + _borderWidth = borderWidth; + emit borderWidthChanged(_borderWidth); + update(); +} + +void TipBackgroundBox::setFill(QColor fill) +{ + if (_fill == fill) + return; + + _fill = fill; + emit fillChanged(_fill); + update(); +} + +void TipBackgroundBox::paint(QPainter *painter) +{ + QPainterPath pp = pathFromArrowAndGeometry(_arrow, QRectF{0.0,0.0,width(), height()}); + painter->setBrush(_fill); + painter->setPen(QPen{_borderColor, static_cast<double>(_borderWidth)}); + painter->drawPath(pp); +} diff --git a/src/GUI/TipBackgroundBox.hxx b/src/GUI/TipBackgroundBox.hxx new file mode 100644 index 000000000..77963b5b9 --- /dev/null +++ b/src/GUI/TipBackgroundBox.hxx @@ -0,0 +1,61 @@ +#pragma once + +#include <QQuickPaintedItem> +#include <QColor> + +#include <GUI/GettingStartedTip.hxx> + +class TipBackgroundBox : public QQuickPaintedItem +{ + Q_OBJECT + + Q_PROPERTY(GettingStartedTip::Arrow arrow READ arrowPosition WRITE setArrowPosition NOTIFY arrowPositionChanged) + + Q_PROPERTY(QColor fill READ fill WRITE setFill NOTIFY fillChanged) + Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged) + Q_PROPERTY(int borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged) + +public: + TipBackgroundBox(QQuickItem* parent = nullptr); + + GettingStartedTip::Arrow arrowPosition() const; + + QColor borderColor() const; + + int borderWidth() const; + + QColor fill() const; + + static int arrowSideOffset(); + + static int arrowHeight(); + +public slots: + void setArrowPosition(GettingStartedTip::Arrow arrow); + + void setBorderColor(QColor borderColor); + + void setBorderWidth(int borderWidth); + + void setFill(QColor fill); + +signals: + void arrowPositionChanged(GettingStartedTip::Arrow arrow); + + void borderColorChanged(QColor borderColor); + + void borderWidthChanged(int borderWidth); + + void fillChanged(QColor fill); + +protected: + void paint(QPainter* painter) override; + +private: + GettingStartedTip::Arrow _arrow = GettingStartedTip::Arrow::TopCenter; + + QColor _borderColor; + int _borderWidth = -1; // off + QColor _fill; +}; + diff --git a/src/GUI/qml/AircraftCompactDelegate.qml b/src/GUI/qml/AircraftCompactDelegate.qml index b97daee98..61bb9ec88 100644 --- a/src/GUI/qml/AircraftCompactDelegate.qml +++ b/src/GUI/qml/AircraftCompactDelegate.qml @@ -107,6 +107,25 @@ Item { model.activeVariant = index root.select(model.uri) } + + GettingStartedTip { + tipId: "aircraftVariantTip" + enabled: model.variantCount > 0 + standalone: true + + Component.onCompleted: { + if (enabled) { + showOneShot(); + } + } + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopCenter + text: qsTr("Click here to select different variants or models of this aircraft") + } } } diff --git a/src/GUI/qml/AircraftDetailsView.qml b/src/GUI/qml/AircraftDetailsView.qml index 5e9ae5df5..4f401992c 100644 --- a/src/GUI/qml/AircraftDetailsView.qml +++ b/src/GUI/qml/AircraftDetailsView.qml @@ -1,6 +1,7 @@ import QtQuick 2.4 import QtQuick.Controls 2.2 import FlightGear.Launcher 1.0 +import FlightGear 1.0 import "." Rectangle { @@ -24,16 +25,20 @@ Rectangle { anchors.fill: parent contentWidth: parent.width - contentHeight: content.childrenRect.height + contentHeight: content.height boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar {} Item { id: content + width: root.width height: childrenRect.height + GettingStartedScope.controller: tipsLayer.controller + Column { + id: contentColumn width: content.width - (Style.margin * 2) spacing: Style.margin anchors.horizontalCenter: parent.horizontalCenter @@ -172,6 +177,15 @@ Rectangle { onToggle: { aircraft.favourite = on; } + + GettingStartedTip { + anchors { + verticalCenter: parent.verticalCenter + left: parent.right + } + arrow: GettingStartedTip.LeftCenter + text: qsTr("Click here to mark this as a favourite aircraft") + } } Grid { @@ -237,7 +251,12 @@ Rectangle { } } // main layout column - } // of main item + GettingStartedTipLayer { + id: tipsLayer + anchors.fill: parent + scopeId: "aircraft-details" + } + } // of main item } // of Flickable } // of Rect diff --git a/src/GUI/qml/AircraftList.qml b/src/GUI/qml/AircraftList.qml index 454e34b37..dda24ca5d 100644 --- a/src/GUI/qml/AircraftList.qml +++ b/src/GUI/qml/AircraftList.qml @@ -1,5 +1,6 @@ import QtQuick 2.2 import FlightGear.Launcher 1.0 as FG +import FlightGear 1.0 import "." // -> forces the qmldir to be loaded FocusScope @@ -30,6 +31,8 @@ FocusScope } } + GettingStartedScope.controller: tipsLayer.controller + Rectangle { id: tabBar @@ -43,6 +46,17 @@ FocusScope anchors.leftMargin: Style.margin gridMode: !_launcher.aircraftGridMode onClicked: _launcher.aircraftGridMode = !_launcher.aircraftGridMode + + GettingStartedTip { + tipId: "gridModeTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopLeft + text: qsTr("Click here to switch between grid and list mode") + } } Row { @@ -57,6 +71,18 @@ FocusScope root.updateSelectionFromLauncher(); } active: root.state == "installed" + + GettingStartedTip { + tipId: "installedAircraftTip" + nextTip: "gridModeTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopCenter + text: qsTr("Use this tab to view installed aircraft") + } } TabButton { @@ -77,6 +103,18 @@ FocusScope root.updateSelectionFromLauncher(); } active: root.state == "browse" + + GettingStartedTip { + tipId: "browseTip" + nextTip: "searchAircraftTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopCenter + text: qsTr("Use this tab to view available aircraft to download") + } } TabButton { @@ -107,6 +145,18 @@ FocusScope } active: root.state == "search" + + GettingStartedTip { + tipId: "searchAircraftTip" + nextTip: "installedAircraftTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopRight + text: qsTr("Enter text here to search aircraft names and descriptions.") + } } } @@ -123,6 +173,7 @@ FocusScope id: ratingsHeader AircraftRatingsPanel { width: aircraftContent.width + tips: tipsLayer onClearSelection: { _launcher.selectedAircraft = ""; root.updateSelectionFromLauncher() @@ -347,6 +398,13 @@ FocusScope detailsView.visible = false; } + // we don't want our tips to interfere with the details views + GettingStartedTipLayer { + id: tipsLayer + anchors.fill: parent + scopeId: "aircraft" + } + AircraftDetailsView { id: detailsView anchors.fill: parent @@ -358,5 +416,7 @@ FocusScope onClicked: root.goBack(); } } + + } diff --git a/src/GUI/qml/AircraftPreviewPanel.qml b/src/GUI/qml/AircraftPreviewPanel.qml index ef9608262..5bda65142 100644 --- a/src/GUI/qml/AircraftPreviewPanel.qml +++ b/src/GUI/qml/AircraftPreviewPanel.qml @@ -1,5 +1,7 @@ import QtQuick 2.4 import FlightGear.Launcher 1.0 +import FlightGear 1.0 +import "." // -> forces the qmldir to be loaded Rectangle { id: root @@ -66,7 +68,7 @@ Rectangle { height: 8 width: 8 radius: 4 - color: (model.index == root.activePreview) ? "white" : "#cfcfcf" + color: (model.index == root.activePreview) ? "white" : Style.themeColor } } } @@ -107,5 +109,14 @@ Rectangle { root.activePreview = Math.min(root.activePreview + 1, root.previews.length - 1) } } + + GettingStartedTip { + anchors { + verticalCenter: parent.verticalCenter + right: parent.left + } + arrow: GettingStartedTip.RightCenter + text: qsTr("Click here to cycle through preview images") + } } } diff --git a/src/GUI/qml/AircraftRatingsPanel.qml b/src/GUI/qml/AircraftRatingsPanel.qml index 9df0585be..87c503c75 100644 --- a/src/GUI/qml/AircraftRatingsPanel.qml +++ b/src/GUI/qml/AircraftRatingsPanel.qml @@ -1,5 +1,7 @@ import QtQuick 2.2 import FlightGear.Launcher 1.0 as FG +import FlightGear 1.0 + import "." ListHeaderBox @@ -7,6 +9,8 @@ ListHeaderBox id: root signal clearSelection(); + GettingStartedScope.controller: tips.controller + contents: [ ToggleSwitch { @@ -45,6 +49,23 @@ ListHeaderBox editRatingsPanel.visible = true } + + Component.onCompleted: { + editTip.showOneShot() + } + + GettingStartedTip { + id: editTip + tipId: "editRatingsTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + standalone: true + arrow: GettingStartedTip.TopRight + text: qsTr("Click here to change which aircraft are shown or hidden based on their ratings") + } }, // mouse are behind panel to consume clicks diff --git a/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml b/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml index 09d240174..b0f2a6647 100644 --- a/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml +++ b/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml @@ -12,7 +12,7 @@ Text { wrapMode: Text.WordWrap font.pixelSize: Style.subHeadingFontPixelSize - color: "white" + color: Style.themeContrastTextColor onLinkActivated: { _launcher.requestUpdateAllAircraft(); diff --git a/src/GUI/qml/GettingStartedTipDisplay.qml b/src/GUI/qml/GettingStartedTipDisplay.qml new file mode 100644 index 000000000..a56d0c5d6 --- /dev/null +++ b/src/GUI/qml/GettingStartedTipDisplay.qml @@ -0,0 +1,134 @@ +import QtQuick 2.4 +import QtQml 2.4 +import FlightGear 1.0 +import "." + + +Item { + id: root + + width: 100 + height: 100 + + property GettingStartedController controller: nil + + property rect contentGeom: controller.contentGeometry + property rect tipGeom: controller.tipGeometry + + // pass the tip height into the controller, when it's valid + Binding { + target: controller + property: "activeTipHeight" + value: contentBox.height + } + + // the visible tip box + TipBackgroundBox { + id: tipBox + + fill: Style.themeColor + borderWidth: 1 + borderColor: Qt.darker(Style.themeColor) + + x: tipGeom.x + y: tipGeom.y + width: tipGeom.width + height: tipGeom.height + arrow: controller.tip.arrow + + Item { + id: contentBox + x: contentGeom.x + y: contentGeom.y + width: contentGeom.width + height: tipText.height + closeText.height + (Style.margin * 3) + + Text { + id: tipText + + font.pixelSize: Style.subHeadingFontPixelSize + color: Style.themeContrastTextColor + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: Style.margin + } + + // size this to the text, after wrapping + height: implicitHeight + + text: controller.tip.text + wrapMode: Text.WordWrap + } + + Text { + anchors { + margins: Style.margin + left: parent.left + top: tipText.bottom + } + + text: "<" + color: prevMouseArea.containsMouse ? Style.themeContrastLinkColor : Style.themeContrastTextColor + visible: (controller.index > 0) + MouseArea { + id: prevMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: { + controller.index = controller.index - 1 + } + } + } + + Text { + id: closeText + + anchors { + margins: Style.margin + horizontalCenter: parent.horizontalCenter + top: tipText.bottom + } + + text: qsTr("Close") + color: closeMouseArea.containsMouse ? Style.themeContrastLinkColor : Style.themeContrastTextColor + width: implicitWidth + horizontalAlignment: Text.AlignHCenter + + MouseArea { + id: closeMouseArea + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: controller.close(); + } + } + + Text { + anchors { + margins: Style.margin + right: parent.right + top: tipText.bottom + } + + text: ">" + color: nextMouseArea.containsMouse ? Style.themeContrastLinkColor : Style.themeContrastTextColor + visible: controller.index < (controller.count - 1) + + MouseArea { + id: nextMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + controller.index = controller.index + 1 + } + cursorShape: Qt.PointingHandCursor + } + } + } // of tip content box + } // of tip background shape +} diff --git a/src/GUI/qml/GettingStartedTipLayer.qml b/src/GUI/qml/GettingStartedTipLayer.qml new file mode 100644 index 000000000..4aa644c20 --- /dev/null +++ b/src/GUI/qml/GettingStartedTipLayer.qml @@ -0,0 +1,45 @@ +import QtQuick 2.4 +import QtQml 2.4 + +import FlightGear 1.0 +import "." + +Item { + id: root + + property GettingStartedController controller: ctl + + property alias scopeId: ctl.scopeId + property alias active: ctl.active + + function showOneShot(tip) { + ctl.showOneShotTip(tip); + } + + GettingStartedController { + id: ctl + visualArea: root + } + + // ensure active-ness is updated, if the 'reset all tips' function is used + Connections { + target: _launcher + onDidResetGettingStartedTips: ctl.tipsWereReset(); + } + + // use a Loader to handle tip display, so we're not creating visual items + // for the tip, in the common case that no tip is displayed. + Loader { + id: load + source: (ctl.active && ctl.tipPositionValid) ? "qrc:///qml/GettingStartedTipDisplay.qml" : "" + x: ctl.tipPositionInVisualArea.x + y: ctl.tipPositionInVisualArea.y + } + + // pass the controller into our loaded item + Binding { + target: load.item + property: "controller" + value: ctl + } +} diff --git a/src/GUI/qml/HelpSupport.qml b/src/GUI/qml/HelpSupport.qml index c37f1db94..a1c97ef10 100644 --- a/src/GUI/qml/HelpSupport.qml +++ b/src/GUI/qml/HelpSupport.qml @@ -2,6 +2,7 @@ import QtQuick 2.4 import QtQuick.Controls 2.2 import FlightGear.Launcher 1.0 +import FlightGear 1.0 import "." Item { @@ -56,6 +57,20 @@ Item { } } + Text { + width: parent.width + font.pixelSize: Style.baseFontPixelSize * 1.5 + color: Style.baseTextColor + wrapMode: Text.WordWrap + + text: qsTr("<p>For help using this launcher, <a %1>try enabling the getting started hints</a>.</p>\n").arg("href=\"enable-tips\""); + + onLinkActivated: { + // reset tips, so they are shown again + _launcher.resetGettingStartedTips(); + } + } + Text { width: parent.width font.pixelSize: Style.baseFontPixelSize * 1.5 diff --git a/src/GUI/qml/ListHeaderBox.qml b/src/GUI/qml/ListHeaderBox.qml index 664dbacea..daf315199 100644 --- a/src/GUI/qml/ListHeaderBox.qml +++ b/src/GUI/qml/ListHeaderBox.qml @@ -7,6 +7,7 @@ Item { height: visible ? contentBox.height + (Style.margin * 2) : 0 z: 100 + property GettingStartedTipLayer tips property alias contents: contentBox.children Rectangle { diff --git a/src/GUI/qml/Section.qml b/src/GUI/qml/Section.qml index 4515b8419..e6ccd7173 100644 --- a/src/GUI/qml/Section.qml +++ b/src/GUI/qml/Section.qml @@ -1,6 +1,7 @@ import QtQuick 2.4 import "." import FlightGear.Launcher 1.0 +import FlightGear 1.0 Item { id: root @@ -11,7 +12,6 @@ Item { property string summary: "" readonly property bool haveAdvancedSettings: anyAdvancedSettings(contents) - implicitWidth: parent.width implicitHeight: headerRect.height + contentBox.height + (Style.margin * 2) @@ -89,6 +89,18 @@ Item { anchors.verticalCenter: parent.verticalCenter height: parent.height visible: root.haveAdvancedSettings + + GettingStartedTip { + tipId: "expandSectionTip" + enabled: root.haveAdvancedSettings + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopRight + text: qsTr("Click here to show advanced settings in this section") + } } } diff --git a/src/GUI/qml/Settings.qml b/src/GUI/qml/Settings.qml index decce99f3..13a3b41be 100644 --- a/src/GUI/qml/Settings.qml +++ b/src/GUI/qml/Settings.qml @@ -2,6 +2,7 @@ import QtQuick 2.4 import QtQuick.Controls 2.2 import FlightGear.Launcher 1.0 +import FlightGear 1.0 import "." Item { @@ -38,6 +39,8 @@ Item { id: sectionColumn width: parent.width + GettingStartedScope.controller: tips.controller + Item { // top margin width: parent.width @@ -67,6 +70,18 @@ Item { onSearch: { _launcher.settingsSearchTerm = term } + + GettingStartedTip { + tipId: "searchSettingsTip" + nextTip: "expandSectionTip" + + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.bottom + } + arrow: GettingStartedTip.TopRight + text: qsTr("Enter text here to search for a setting") + } } } @@ -490,5 +505,11 @@ Item { ] } } // of Column + + GettingStartedTipLayer { + id: tips + anchors.fill: parent + scopeId: "settings" + } } // of Flickable } diff --git a/src/GUI/qml/Summary.qml b/src/GUI/qml/Summary.qml index 10f97f5bb..3a313fb88 100644 --- a/src/GUI/qml/Summary.qml +++ b/src/GUI/qml/Summary.qml @@ -1,6 +1,8 @@ import QtQuick 2.4 import QtQml 2.4 import FlightGear.Launcher 1.0 +import FlightGear 1.0 + import "." Item { @@ -10,6 +12,8 @@ Item { signal showSelectedLocation(); signal showFlightPlan(); + GettingStartedScope.controller: tips.controller + Rectangle { anchors.fill: parent color: "#7f7f7f" @@ -166,6 +170,18 @@ Item { onSelected: { _launcher.selectedAircraft = _launcher.aircraftHistory.uriAt(index) } + + GettingStartedTip { + tipId: "aircraftHistoryTip" + nextTip: "locationHistoryTip" + arrow: GettingStartedTip.BottomRight + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.top + } + text: qsTr("Click here to select a recently used aircraft.") + } } // empty space in next row (thumbnail, long aircraft description) @@ -240,6 +256,27 @@ Item { _launcher.selectedAircraftState = model.tagForState(index); } } + + GettingStartedTip { + id: aircraftStateTip + tipId: "aircraftStateTip" + arrow: GettingStartedTip.LeftCenter + enabled: stateSelectionGroup.visible + standalone: true + + // show as a one-shot tip, first time we're enabled + onEnabledChanged: { + if (enabled) { + tips.showOneShot(this) + } + } + + x: parent.implicitWidth + anchors { + verticalCenter: parent.verticalCenter + } + text: qsTr("Use this menu to choose the starting state of the aircraft") + } } StyledText { @@ -282,6 +319,17 @@ Item { font.pixelSize: Style.headingFontPixelSize width: summaryGrid.middleColumnWidth onClicked: root.showSelectedLocation() + + GettingStartedTip { + tipId: "currentLocationTextTip" + anchors { + top: parent.bottom + left: parent.left + leftMargin: Style.strutSize + } + arrow: GettingStartedTip.TopLeft + text: qsTr("Click this description to view and change the current location.") + } } HistoryPopup { @@ -291,6 +339,16 @@ Item { onSelected: { _launcher.restoreLocation(_launcher.locationHistory.locationAt(index)) } + + GettingStartedTip { + tipId: "locationHistoryTip" + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.top + } + arrow: GettingStartedTip.BottomRight + text: qsTr("Click here to access recently used locations") + } } // flight plan summary row @@ -338,4 +396,10 @@ Item { } } } // of summary box + + GettingStartedTipLayer { + id: tips + anchors.fill: parent + scopeId: "summary" + } } diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc index 052229e73..2aeda27af 100644 --- a/src/GUI/resources.qrc +++ b/src/GUI/resources.qrc @@ -146,6 +146,8 @@ <file>qml/DownloadsInDocumentsWarning.qml</file> <file>qml/BackButton.qml</file> <file>qml/ScrollToBottomHint.qml</file> + <file>qml/GettingStartedTipLayer.qml</file> + <file>qml/GettingStartedTipDisplay.qml</file> </qresource> <qresource prefix="/preview"> <file alias="close-icon">assets/preview-close.png</file>