1
0
Fork 0

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.
This commit is contained in:
Automatic Release Builder 2020-11-10 11:09:46 +00:00 committed by James Turner
parent 89de1defa0
commit 6b6defbead
27 changed files with 1660 additions and 5 deletions

View file

@ -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}
)

View file

@ -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();
}

View file

@ -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)

View file

@ -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();
}
}

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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);

View file

@ -28,6 +28,7 @@
#include "LocationController.hxx"
#include "QtLauncher.hxx"
#include "UpdateChecker.hxx"
#include "GettingStartedTip.hxx"
//////////////////////////////////////////////////////////////////////////////

View file

@ -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();

View file

@ -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);
}

View file

@ -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;
};

View file

@ -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")
}
}
}

View file

@ -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

View file

@ -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();
}
}
}

View file

@ -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")
}
}
}

View file

@ -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

View file

@ -12,7 +12,7 @@ Text {
wrapMode: Text.WordWrap
font.pixelSize: Style.subHeadingFontPixelSize
color: "white"
color: Style.themeContrastTextColor
onLinkActivated: {
_launcher.requestUpdateAllAircraft();

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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

View file

@ -7,6 +7,7 @@ Item {
height: visible ? contentBox.height + (Style.margin * 2) : 0
z: 100
property GettingStartedTipLayer tips
property alias contents: contentBox.children
Rectangle {

View file

@ -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")
}
}
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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>