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:
parent
89de1defa0
commit
6b6defbead
27 changed files with 1660 additions and 5 deletions
src/GUI
CMakeLists.txtGettingStartedScope.cxxGettingStartedScope.hxxGettingStartedTip.cxxGettingStartedTip.hxxGettingStartedTipsController.cxxGettingStartedTipsController.hxxLauncherController.cxxLauncherController.hxxLauncherMainWindow.cxxQtLauncher.cxxTipBackgroundBox.cxxTipBackgroundBox.hxx
qml
AircraftCompactDelegate.qmlAircraftDetailsView.qmlAircraftList.qmlAircraftPreviewPanel.qmlAircraftRatingsPanel.qmlDidMigrateOfficialCatalogNotification.qmlGettingStartedTipDisplay.qmlGettingStartedTipLayer.qmlHelpSupport.qmlListHeaderBox.qmlSection.qmlSettings.qmlSummary.qml
resources.qrc
|
@ -201,6 +201,14 @@ if (HAVE_QT)
|
||||||
PathUrlHelper.hxx
|
PathUrlHelper.hxx
|
||||||
DialogStateController.cxx
|
DialogStateController.cxx
|
||||||
DialogStateController.hxx
|
DialogStateController.hxx
|
||||||
|
GettingStartedTip.hxx
|
||||||
|
GettingStartedTip.cxx
|
||||||
|
GettingStartedTipsController.cxx
|
||||||
|
GettingStartedTipsController.hxx
|
||||||
|
TipBackgroundBox.cxx
|
||||||
|
TipBackgroundBox.hxx
|
||||||
|
GettingStartedScope.hxx
|
||||||
|
GettingStartedScope.cxx
|
||||||
${QQUI_SOURCES}
|
${QQUI_SOURCES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
29
src/GUI/GettingStartedScope.cxx
Normal file
29
src/GUI/GettingStartedScope.cxx
Normal 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();
|
||||||
|
}
|
43
src/GUI/GettingStartedScope.hxx
Normal file
43
src/GUI/GettingStartedScope.hxx
Normal 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)
|
147
src/GUI/GettingStartedTip.cxx
Normal file
147
src/GUI/GettingStartedTip.cxx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
110
src/GUI/GettingStartedTip.hxx
Normal file
110
src/GUI/GettingStartedTip.hxx
Normal 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
|
463
src/GUI/GettingStartedTipsController.cxx
Executable file
463
src/GUI/GettingStartedTipsController.cxx
Executable 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"
|
150
src/GUI/GettingStartedTipsController.hxx
Normal file
150
src/GUI/GettingStartedTipsController.hxx
Normal 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
|
|
@ -59,6 +59,10 @@
|
||||||
#include "ThumbnailImageItem.hxx"
|
#include "ThumbnailImageItem.hxx"
|
||||||
#include "UnitsModel.hxx"
|
#include "UnitsModel.hxx"
|
||||||
#include "UpdateChecker.hxx"
|
#include "UpdateChecker.hxx"
|
||||||
|
#include "GettingStartedTipsController.hxx"
|
||||||
|
#include "GettingStartedTip.hxx"
|
||||||
|
#include "TipBackgroundBox.hxx"
|
||||||
|
#include "GettingStartedScope.hxx"
|
||||||
|
|
||||||
using namespace simgear::pkg;
|
using namespace simgear::pkg;
|
||||||
|
|
||||||
|
@ -199,6 +203,11 @@ void LauncherController::initQML()
|
||||||
|
|
||||||
qmlRegisterSingletonType(QUrl("qrc:/qml/OverlayShared.qml"), "FlightGear", 1, 0, "OverlayShared");
|
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);
|
QNetworkDiskCache* diskCache = new QNetworkDiskCache(this);
|
||||||
SGPath cachePath = globals->get_fg_home() / "PreviewsCache";
|
SGPath cachePath = globals->get_fg_home() / "PreviewsCache";
|
||||||
diskCache->setCacheDirectory(QString::fromStdString(cachePath.utf8Str()));
|
diskCache->setCacheDirectory(QString::fromStdString(cachePath.utf8Str()));
|
||||||
|
@ -876,6 +885,18 @@ void LauncherController::setAircraftGridMode(bool aircraftGridMode)
|
||||||
emit aircraftGridModeChanged(m_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)
|
void LauncherController::setMinWindowSize(QSize sz)
|
||||||
{
|
{
|
||||||
if (sz == m_minWindowSize)
|
if (sz == m_minWindowSize)
|
||||||
|
|
|
@ -243,6 +243,7 @@ signals:
|
||||||
void inAppChanged();
|
void inAppChanged();
|
||||||
void installedAircraftCountChanged(int installedAircraftCount);
|
void installedAircraftCountChanged(int installedAircraftCount);
|
||||||
|
|
||||||
|
void didResetGettingStartedTips();
|
||||||
public slots:
|
public slots:
|
||||||
void setSelectedAircraft(QUrl selectedAircraft);
|
void setSelectedAircraft(QUrl selectedAircraft);
|
||||||
|
|
||||||
|
@ -263,6 +264,8 @@ public slots:
|
||||||
void saveConfigAs();
|
void saveConfigAs();
|
||||||
void setAircraftGridMode(bool aircraftGridMode);
|
void setAircraftGridMode(bool aircraftGridMode);
|
||||||
|
|
||||||
|
void resetGettingStartedTips();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
void onAircraftInstalledCompleted(QModelIndex index);
|
void onAircraftInstalledCompleted(QModelIndex index);
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include "LocationController.hxx"
|
#include "LocationController.hxx"
|
||||||
#include "QtLauncher.hxx"
|
#include "QtLauncher.hxx"
|
||||||
#include "UpdateChecker.hxx"
|
#include "UpdateChecker.hxx"
|
||||||
|
#include "GettingStartedTip.hxx"
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@
|
||||||
#include "LocalAircraftCache.hxx"
|
#include "LocalAircraftCache.hxx"
|
||||||
#include "PathListModel.hxx"
|
#include "PathListModel.hxx"
|
||||||
#include "UnitsModel.hxx"
|
#include "UnitsModel.hxx"
|
||||||
|
#include "GettingStartedTip.hxx"
|
||||||
|
|
||||||
#if defined(SG_MAC)
|
#if defined(SG_MAC)
|
||||||
#include <GUI/CocoaHelpers.h>
|
#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.");
|
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);
|
QString m = qApp->translate("initNavCache", waitForOtherMsg);
|
||||||
|
|
||||||
|
addSentryBreadcrumb("Launcher: showing wait for other process NavCache rebuild dialog", "info");
|
||||||
QProgressDialog waitForRebuild(m,
|
QProgressDialog waitForRebuild(m,
|
||||||
QString() /* cancel text */,
|
QString() /* cancel text */,
|
||||||
0, 0, Q_NULLPTR,
|
0, 0, Q_NULLPTR,
|
||||||
|
@ -133,6 +135,7 @@ void initNavCache()
|
||||||
updateTimer.start(); // timer won't actually run until we process events
|
updateTimer.start(); // timer won't actually run until we process events
|
||||||
waitForRebuild.exec();
|
waitForRebuild.exec();
|
||||||
updateTimer.stop();
|
updateTimer.stop();
|
||||||
|
addSentryBreadcrumb("Launcher: done waiting for other process NavCache rebuild dialog", "info");
|
||||||
}
|
}
|
||||||
|
|
||||||
NavDataCache* cache = NavDataCache::createInstance();
|
NavDataCache* cache = NavDataCache::createInstance();
|
||||||
|
|
192
src/GUI/TipBackgroundBox.cxx
Normal file
192
src/GUI/TipBackgroundBox.cxx
Normal 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);
|
||||||
|
}
|
61
src/GUI/TipBackgroundBox.hxx
Normal file
61
src/GUI/TipBackgroundBox.hxx
Normal 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;
|
||||||
|
};
|
||||||
|
|
|
@ -107,6 +107,25 @@ Item {
|
||||||
model.activeVariant = index
|
model.activeVariant = index
|
||||||
root.select(model.uri)
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.4
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -24,16 +25,20 @@ Rectangle {
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentWidth: parent.width
|
contentWidth: parent.width
|
||||||
contentHeight: content.childrenRect.height
|
contentHeight: content.height
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
ScrollBar.vertical: ScrollBar {}
|
ScrollBar.vertical: ScrollBar {}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
width: root.width
|
width: root.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
|
|
||||||
|
GettingStartedScope.controller: tipsLayer.controller
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: contentColumn
|
||||||
width: content.width - (Style.margin * 2)
|
width: content.width - (Style.margin * 2)
|
||||||
spacing: Style.margin
|
spacing: Style.margin
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
@ -172,6 +177,15 @@ Rectangle {
|
||||||
onToggle: {
|
onToggle: {
|
||||||
aircraft.favourite = on;
|
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 {
|
Grid {
|
||||||
|
@ -237,7 +251,12 @@ Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
} // main layout column
|
} // main layout column
|
||||||
} // of main item
|
|
||||||
|
|
||||||
|
GettingStartedTipLayer {
|
||||||
|
id: tipsLayer
|
||||||
|
anchors.fill: parent
|
||||||
|
scopeId: "aircraft-details"
|
||||||
|
}
|
||||||
|
} // of main item
|
||||||
} // of Flickable
|
} // of Flickable
|
||||||
} // of Rect
|
} // of Rect
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
import FlightGear.Launcher 1.0 as FG
|
import FlightGear.Launcher 1.0 as FG
|
||||||
|
import FlightGear 1.0
|
||||||
import "." // -> forces the qmldir to be loaded
|
import "." // -> forces the qmldir to be loaded
|
||||||
|
|
||||||
FocusScope
|
FocusScope
|
||||||
|
@ -30,6 +31,8 @@ FocusScope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GettingStartedScope.controller: tipsLayer.controller
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
id: tabBar
|
id: tabBar
|
||||||
|
@ -43,6 +46,17 @@ FocusScope
|
||||||
anchors.leftMargin: Style.margin
|
anchors.leftMargin: Style.margin
|
||||||
gridMode: !_launcher.aircraftGridMode
|
gridMode: !_launcher.aircraftGridMode
|
||||||
onClicked: _launcher.aircraftGridMode = !_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 {
|
Row {
|
||||||
|
@ -57,6 +71,18 @@ FocusScope
|
||||||
root.updateSelectionFromLauncher();
|
root.updateSelectionFromLauncher();
|
||||||
}
|
}
|
||||||
active: root.state == "installed"
|
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 {
|
TabButton {
|
||||||
|
@ -77,6 +103,18 @@ FocusScope
|
||||||
root.updateSelectionFromLauncher();
|
root.updateSelectionFromLauncher();
|
||||||
}
|
}
|
||||||
active: root.state == "browse"
|
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 {
|
TabButton {
|
||||||
|
@ -107,6 +145,18 @@ FocusScope
|
||||||
}
|
}
|
||||||
|
|
||||||
active: root.state == "search"
|
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
|
id: ratingsHeader
|
||||||
AircraftRatingsPanel {
|
AircraftRatingsPanel {
|
||||||
width: aircraftContent.width
|
width: aircraftContent.width
|
||||||
|
tips: tipsLayer
|
||||||
onClearSelection: {
|
onClearSelection: {
|
||||||
_launcher.selectedAircraft = "";
|
_launcher.selectedAircraft = "";
|
||||||
root.updateSelectionFromLauncher()
|
root.updateSelectionFromLauncher()
|
||||||
|
@ -347,6 +398,13 @@ FocusScope
|
||||||
detailsView.visible = false;
|
detailsView.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't want our tips to interfere with the details views
|
||||||
|
GettingStartedTipLayer {
|
||||||
|
id: tipsLayer
|
||||||
|
anchors.fill: parent
|
||||||
|
scopeId: "aircraft"
|
||||||
|
}
|
||||||
|
|
||||||
AircraftDetailsView {
|
AircraftDetailsView {
|
||||||
id: detailsView
|
id: detailsView
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -358,5 +416,7 @@ FocusScope
|
||||||
onClicked: root.goBack();
|
onClicked: root.goBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.4
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
|
import "." // -> forces the qmldir to be loaded
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
@ -66,7 +68,7 @@ Rectangle {
|
||||||
height: 8
|
height: 8
|
||||||
width: 8
|
width: 8
|
||||||
radius: 4
|
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)
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
import FlightGear.Launcher 1.0 as FG
|
import FlightGear.Launcher 1.0 as FG
|
||||||
|
import FlightGear 1.0
|
||||||
|
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
ListHeaderBox
|
ListHeaderBox
|
||||||
|
@ -7,6 +9,8 @@ ListHeaderBox
|
||||||
id: root
|
id: root
|
||||||
signal clearSelection();
|
signal clearSelection();
|
||||||
|
|
||||||
|
GettingStartedScope.controller: tips.controller
|
||||||
|
|
||||||
contents: [
|
contents: [
|
||||||
|
|
||||||
ToggleSwitch {
|
ToggleSwitch {
|
||||||
|
@ -45,6 +49,23 @@ ListHeaderBox
|
||||||
editRatingsPanel.visible = true
|
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
|
// mouse are behind panel to consume clicks
|
||||||
|
|
|
@ -12,7 +12,7 @@ Text {
|
||||||
|
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
font.pixelSize: Style.subHeadingFontPixelSize
|
font.pixelSize: Style.subHeadingFontPixelSize
|
||||||
color: "white"
|
color: Style.themeContrastTextColor
|
||||||
|
|
||||||
onLinkActivated: {
|
onLinkActivated: {
|
||||||
_launcher.requestUpdateAllAircraft();
|
_launcher.requestUpdateAllAircraft();
|
||||||
|
|
134
src/GUI/qml/GettingStartedTipDisplay.qml
Normal file
134
src/GUI/qml/GettingStartedTipDisplay.qml
Normal 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
|
||||||
|
}
|
45
src/GUI/qml/GettingStartedTipLayer.qml
Normal file
45
src/GUI/qml/GettingStartedTipLayer.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import QtQuick 2.4
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
Item {
|
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 {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
font.pixelSize: Style.baseFontPixelSize * 1.5
|
font.pixelSize: Style.baseFontPixelSize * 1.5
|
||||||
|
|
|
@ -7,6 +7,7 @@ Item {
|
||||||
height: visible ? contentBox.height + (Style.margin * 2) : 0
|
height: visible ? contentBox.height + (Style.margin * 2) : 0
|
||||||
z: 100
|
z: 100
|
||||||
|
|
||||||
|
property GettingStartedTipLayer tips
|
||||||
property alias contents: contentBox.children
|
property alias contents: contentBox.children
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.4
|
||||||
import "."
|
import "."
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
@ -11,7 +12,6 @@ Item {
|
||||||
property string summary: ""
|
property string summary: ""
|
||||||
readonly property bool haveAdvancedSettings: anyAdvancedSettings(contents)
|
readonly property bool haveAdvancedSettings: anyAdvancedSettings(contents)
|
||||||
|
|
||||||
|
|
||||||
implicitWidth: parent.width
|
implicitWidth: parent.width
|
||||||
implicitHeight: headerRect.height + contentBox.height + (Style.margin * 2)
|
implicitHeight: headerRect.height + contentBox.height + (Style.margin * 2)
|
||||||
|
|
||||||
|
@ -89,6 +89,18 @@ Item {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
height: parent.height
|
height: parent.height
|
||||||
visible: root.haveAdvancedSettings
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import QtQuick 2.4
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -38,6 +39,8 @@ Item {
|
||||||
id: sectionColumn
|
id: sectionColumn
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
|
GettingStartedScope.controller: tips.controller
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
// top margin
|
// top margin
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
@ -67,6 +70,18 @@ Item {
|
||||||
onSearch: {
|
onSearch: {
|
||||||
_launcher.settingsSearchTerm = term
|
_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
|
} // of Column
|
||||||
|
|
||||||
|
GettingStartedTipLayer {
|
||||||
|
id: tips
|
||||||
|
anchors.fill: parent
|
||||||
|
scopeId: "settings"
|
||||||
|
}
|
||||||
} // of Flickable
|
} // of Flickable
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import QtQuick 2.4
|
import QtQuick 2.4
|
||||||
import QtQml 2.4
|
import QtQml 2.4
|
||||||
import FlightGear.Launcher 1.0
|
import FlightGear.Launcher 1.0
|
||||||
|
import FlightGear 1.0
|
||||||
|
|
||||||
import "."
|
import "."
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -10,6 +12,8 @@ Item {
|
||||||
signal showSelectedLocation();
|
signal showSelectedLocation();
|
||||||
signal showFlightPlan();
|
signal showFlightPlan();
|
||||||
|
|
||||||
|
GettingStartedScope.controller: tips.controller
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "#7f7f7f"
|
color: "#7f7f7f"
|
||||||
|
@ -166,6 +170,18 @@ Item {
|
||||||
onSelected: {
|
onSelected: {
|
||||||
_launcher.selectedAircraft = _launcher.aircraftHistory.uriAt(index)
|
_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)
|
// empty space in next row (thumbnail, long aircraft description)
|
||||||
|
@ -240,6 +256,27 @@ Item {
|
||||||
_launcher.selectedAircraftState = model.tagForState(index);
|
_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 {
|
StyledText {
|
||||||
|
@ -282,6 +319,17 @@ Item {
|
||||||
font.pixelSize: Style.headingFontPixelSize
|
font.pixelSize: Style.headingFontPixelSize
|
||||||
width: summaryGrid.middleColumnWidth
|
width: summaryGrid.middleColumnWidth
|
||||||
onClicked: root.showSelectedLocation()
|
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 {
|
HistoryPopup {
|
||||||
|
@ -291,6 +339,16 @@ Item {
|
||||||
onSelected: {
|
onSelected: {
|
||||||
_launcher.restoreLocation(_launcher.locationHistory.locationAt(index))
|
_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
|
// flight plan summary row
|
||||||
|
@ -338,4 +396,10 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // of summary box
|
} // of summary box
|
||||||
|
|
||||||
|
GettingStartedTipLayer {
|
||||||
|
id: tips
|
||||||
|
anchors.fill: parent
|
||||||
|
scopeId: "summary"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,8 @@
|
||||||
<file>qml/DownloadsInDocumentsWarning.qml</file>
|
<file>qml/DownloadsInDocumentsWarning.qml</file>
|
||||||
<file>qml/BackButton.qml</file>
|
<file>qml/BackButton.qml</file>
|
||||||
<file>qml/ScrollToBottomHint.qml</file>
|
<file>qml/ScrollToBottomHint.qml</file>
|
||||||
|
<file>qml/GettingStartedTipLayer.qml</file>
|
||||||
|
<file>qml/GettingStartedTipDisplay.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/preview">
|
<qresource prefix="/preview">
|
||||||
<file alias="close-icon">assets/preview-close.png</file>
|
<file alias="close-icon">assets/preview-close.png</file>
|
||||||
|
|
Loading…
Add table
Reference in a new issue