1
0
Fork 0

Launcher: UI feedback for hangar migrations

Add pop-up notification when we migrate to a new version of a hangar.
Convert the ‘new version available’ message to use a pop-up
notification as well.
This commit is contained in:
Automatic Release Builder 2020-10-25 18:24:33 +00:00 committed by James Turner
parent c71f281a58
commit df42c6f026
18 changed files with 580 additions and 65 deletions

View file

@ -132,6 +132,10 @@ if (HAVE_QT)
FavouriteAircraftData.hxx
UpdateChecker.cxx
UpdateChecker.hxx
LauncherPackageDelegate.hxx
LauncherPackageDelegate.cxx
LauncherNotificationsController.hxx
LauncherNotificationsController.cxx
${uic_sources}
${qrc_sources}
${qml_sources})

View file

@ -17,14 +17,16 @@
#include <QQmlFileSelector>
// launcher headers
#include "QtLauncher.hxx"
#include "AddOnsController.hxx"
#include "AircraftItemModel.hxx"
#include "AircraftModel.hxx"
#include "DefaultAircraftLocator.hxx"
#include "LaunchConfig.hxx"
#include "LocalAircraftCache.hxx"
#include "LauncherController.hxx"
#include "LauncherNotificationsController.hxx"
#include "LauncherPackageDelegate.hxx"
#include "LocalAircraftCache.hxx"
#include "LocationController.hxx"
#include "QtLauncher.hxx"
#include "UpdateChecker.hxx"
//////////////////////////////////////////////////////////////////////////////
@ -80,6 +82,8 @@ LauncherMainWindow::LauncherMainWindow(bool inSimMode) : QQuickView()
connect(qa, &QAction::triggered, m_controller, &LauncherController::quit);
}
connect(this, &QQuickView::statusChanged, this, &LauncherMainWindow::onQuickStatusChanged);
m_controller->initialRestoreSettings();
////////////
@ -103,6 +107,12 @@ LauncherMainWindow::LauncherMainWindow(bool inSimMode) : QQuickView()
auto updater = new UpdateChecker(this);
ctx->setContextProperty("_updates", updater);
auto packageDelegate = new LauncherPackageDelegate(this);
ctx->setContextProperty("_packages", packageDelegate);
auto notifications = new LauncherNotificationsController{this, engine()};
ctx->setContextProperty("_notifications", notifications);
if (!inSimMode) {
auto addOnsCtl = new AddOnsController(this, m_controller->config());
ctx->setContextProperty("_addOns", addOnsCtl);
@ -120,25 +130,22 @@ LauncherMainWindow::LauncherMainWindow(bool inSimMode) : QQuickView()
setSource(QUrl("qrc:///qml/Launcher.qml"));
}
#if 0
void LauncherMainWindow::onQuickStatusChanged(QQuickWidget::Status status)
void LauncherMainWindow::onQuickStatusChanged(QQuickView::Status status)
{
if (status == QQuickWidget::Error) {
QQuickWidget* qw = qobject_cast<QQuickWidget*>(sender());
if (status == QQuickView::Error) {
QString errorString;
Q_FOREACH(auto err, qw->errors()) {
Q_FOREACH (auto err, errors()) {
errorString.append("\n" + err.toString());
}
QMessageBox::critical(this, "UI loading failures.",
tr("Problems occurred loading the user interface. This is often due to missing modules on your system. "
QMessageBox::critical(nullptr, "UI loading failures.",
tr("Problems occurred loading the user interface. This is usually due to missing modules on your system. "
"Please report this error to the FlightGear developer list or forum, and take care to mention your system "
"distribution, etc. Please also include the information provided below.\n")
+ errorString);
"distribution, etc. Please also include the information provided below.\n") +
errorString);
}
}
#endif
LauncherMainWindow::~LauncherMainWindow()
{

View file

@ -48,6 +48,10 @@ public:
bool wasRejected();
bool event(QEvent *event) override;
private slots:
void onQuickStatusChanged(QQuickView::Status status);
private:
LauncherController* m_controller;
};

View file

@ -0,0 +1,167 @@
// Written by James Turner, started October 2020
//
// Copyright (C) 2020 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "LauncherNotificationsController.hxx"
#include <QAbstractListModel>
#include <QDebug>
#include <QQmlEngine>
#include <QSettings>
static LauncherNotificationsController* static_instance = nullptr;
namespace {
const int IdRole = Qt::UserRole + 1;
const int SourceRole = Qt::UserRole + 2;
const int ArgsRole = Qt::UserRole + 3;
} // namespace
class LauncherNotificationsController::NotificationsModel : public QAbstractListModel
{
public:
int rowCount(const QModelIndex&) const override
{
return static_cast<int>(_data.size());
}
QVariant data(const QModelIndex& index, int role) const override
{
const int row = index.row();
if ((row < 0) || (row >= static_cast<int>(_data.size()))) {
return {};
}
const auto& d = _data.at(row);
switch (role) {
case IdRole: return d.id;
case SourceRole: return d.source;
case ArgsRole: return QVariant::fromValue(d.args);
default:
break;
}
return {};
}
QHash<int, QByteArray> roleNames() const override
{
QHash<int, QByteArray> result = QAbstractListModel::roleNames();
result[IdRole] = "id";
result[SourceRole] = "source";
result[ArgsRole] = "args";
return result;
}
void removeIndex(int index)
{
beginRemoveRows({}, index, index);
_data.erase(_data.begin() + index);
endRemoveRows();
}
void append(QString id, QUrl source, QJSValue args)
{
const int newRow = static_cast<int>(_data.size());
beginInsertRows({}, newRow, newRow);
_data.push_back({id, source, args});
endInsertRows();
}
struct Data {
QString id;
QUrl source;
QJSValue args;
};
std::vector<Data> _data;
};
LauncherNotificationsController::LauncherNotificationsController(QObject* pr, QQmlEngine* engine) : QObject(pr)
{
Q_ASSERT(static_instance == nullptr);
static_instance = this;
_model = new NotificationsModel;
_qmlEngine = engine;
}
LauncherNotificationsController::~LauncherNotificationsController()
{
static_instance = nullptr;
}
LauncherNotificationsController* LauncherNotificationsController::instance()
{
return static_instance;
}
QAbstractItemModel* LauncherNotificationsController::notifications() const
{
return _model;
}
QJSValue LauncherNotificationsController::argsForIndex(int index) const
{
if ((index < 0) || (index >= static_cast<int>(_model->_data.size()))) {
return {};
}
const auto& d = _model->_data.at(index);
qDebug() << Q_FUNC_INFO << index;
return d.args;
}
QJSEngine* LauncherNotificationsController::jsEngine()
{
return _qmlEngine;
}
void LauncherNotificationsController::dismissIndex(int index)
{
const auto& d = _model->_data.at(index);
// if the notificsation supports persistent dismissal, then record this
// fact in the global settings, so we don't show it again.
// restore defaults will of course clear these settings, but that's
// desirable anyway.
if (d.args.property("persistent-dismiss").toBool()) {
QSettings settings;
settings.beginGroup("dismissed-notifications");
settings.setValue(d.id, true);
}
_model->removeIndex(index);
}
void LauncherNotificationsController::postNotification(QString id, QUrl source, QJSValue args)
{
const bool supportsPersistentDismiss = args.property("persistent-dismiss").toBool();
if (supportsPersistentDismiss) {
QSettings settings;
settings.beginGroup("dismissed-notifications");
bool alreadyDimissed = settings.value(id).toBool();
if (alreadyDimissed) {
qWarning() << "Skipping notification" << id << ", was previousl dimissed by user";
return;
}
}
_model->append(id, source, args);
}

View file

@ -0,0 +1,59 @@
// Written by James Turner, started October 2020
//
// Copyright (C) 2020 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#pragma once
#include <QJSValue>
#include <QObject>
#include <QUrl>
// forward decls
class QAbstractItemModel;
class QJSEngine;
class QQmlEngine;
class LauncherNotificationsController : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractItemModel* active READ notifications CONSTANT)
public:
LauncherNotificationsController(QObject* pr, QQmlEngine* qmlEngine);
~LauncherNotificationsController();
static LauncherNotificationsController* instance();
QAbstractItemModel* notifications() const;
Q_INVOKABLE QJSValue argsForIndex(int index) const;
QJSEngine* jsEngine();
public slots:
void dismissIndex(int index);
void postNotification(QString id, QUrl source, QJSValue args = {});
signals:
private:
class NotificationsModel;
NotificationsModel* _model = nullptr;
QQmlEngine* _qmlEngine = nullptr;
};

View file

@ -0,0 +1,73 @@
// Copyright (C) 2020 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "config.h"
#include "LauncherPackageDelegate.hxx"
#include <QDebug>
#include <QJSEngine>
#include <simgear/package/Root.hxx>
#include <Main/globals.hxx>
#include <Network/HTTPClient.hxx>
#include "LauncherNotificationsController.hxx"
LauncherPackageDelegate::LauncherPackageDelegate(QObject* parent) : QObject(parent)
{
globals->packageRoot()->addDelegate(this);
const auto http = globals->get_subsystem<FGHTTPClient>();
_defaultCatalogId = http->getDefaultCatalogId();
}
LauncherPackageDelegate::~LauncherPackageDelegate()
{
globals->packageRoot()->removeDelegate(this);
}
void LauncherPackageDelegate::catalogRefreshed(simgear::pkg::CatalogRef aCatalog, simgear::pkg::Delegate::StatusCode aReason)
{
if ((aReason != Delegate::STATUS_REFRESHED) || !aCatalog) {
return;
}
auto nc = LauncherNotificationsController::instance();
if (aCatalog->migratedFrom() != simgear::pkg::CatalogRef{}) {
QJSValue args = nc->jsEngine()->newObject();
args.setProperty("newCatalogName", QString::fromStdString(aCatalog->name()));
if (aCatalog->id() == _defaultCatalogId) {
nc->postNotification("did-migrate-official-catalog-to-" + QString::fromStdString(_defaultCatalogId),
QUrl{"qrc:///qml/DidMigrateOfficialCatalogNotification.qml"},
args);
} else {
nc->postNotification("did-migrate-catalog-to-" + QString::fromStdString(aCatalog->id()),
QUrl{"qrc:///qml/DidMigrateOtherCatalogNotification.qml"},
args);
}
}
}
void LauncherPackageDelegate::finishInstall(simgear::pkg::InstallRef ref, simgear::pkg::Delegate::StatusCode status)
{
Q_UNUSED(ref)
Q_UNUSED(status)
}

View file

@ -0,0 +1,37 @@
#ifndef LAUNCHERPACKAGEDELEGATE_HXX
#define LAUNCHERPACKAGEDELEGATE_HXX
#include <string>
#include <QObject>
#include <simgear/package/Delegate.hxx>
#include <simgear/package/Install.hxx>
#include <simgear/package/Package.hxx>
#include <simgear/package/Root.hxx>
class LauncherPackageDelegate : public QObject,
public simgear::pkg::Delegate
{
Q_OBJECT
public:
explicit LauncherPackageDelegate(QObject* parent = nullptr);
~LauncherPackageDelegate();
protected:
void catalogRefreshed(simgear::pkg::CatalogRef aCatalog, StatusCode aReason) override;
// mandatory overrides, not actually needed here.
void startInstall(simgear::pkg::InstallRef) override {}
void installProgress(simgear::pkg::InstallRef, unsigned int, unsigned int) override{};
void finishInstall(simgear::pkg::InstallRef ref, StatusCode status) override;
signals:
void didMigrateOfficialHangarChanged();
private:
std::string _defaultCatalogId;
};
#endif // LAUNCHERPACKAGEDELEGATE_HXX

View file

@ -135,6 +135,7 @@ void UpdateChecker::receivedUpdateXML(QByteArray body)
const auto s = body.toStdString();
QSettings settings;
auto nc = LauncherNotificationsController::instance();
try {
const char* buffer = s.c_str();
@ -156,6 +157,8 @@ void UpdateChecker::receivedUpdateXML(QByteArray body)
m_updateUri = QUrl(QString::fromStdString(newVersionUri));
emit statusChanged(m_status);
nc->postNotification("flightgear-update-major", QUrl{"qrc:///qml/NewVersionNotification.qml"});
return; // don't consider minor updates
}
}
@ -172,6 +175,8 @@ void UpdateChecker::receivedUpdateXML(QByteArray body)
const std::string newVersionUri = props->getStringValue("download-uri");
m_updateUri = QUrl(QString::fromStdString(newVersionUri));
emit statusChanged(m_status);
nc->postNotification("flightgear-update-point", QUrl{"qrc:///qml/NewVersionNotification.qml"});
}
}
} catch (const sg_exception &e) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

View file

@ -0,0 +1,21 @@
import QtQuick 2.4
import "."
Text {
signal dismiss();
readonly property string updateAllLink: "\"launcher:update-all\"";
readonly property string newName: _notifications.argsForIndex(model.index).newCatalogName
text: qsTr("An updated version of the official aircraft hangar '%2' was automatically installed. " +
"Existing aircraft have been marked for update, <a href=%1>click here to update them all</a>").arg(updateAllLink).arg(newName)
wrapMode: Text.WordWrap
font.pixelSize: Style.subHeadingFontPixelSize
color: "white"
onLinkActivated: {
_launcher.requestUpdateAllAircraft();
dismiss(); // request our dismissal
}
}

View file

@ -0,0 +1,21 @@
import QtQuick 2.4
import "."
Text {
signal dismiss();
readonly property string updateAllLink: "\"launcher:update-all\"";
readonly property string newName: _notifications.argsForIndex(model.index).newCatalogName
text: qsTr("An updated version of the hangar '%2' was automatically installed. " +
"Existing aircraft have been marked for update, <a href=%1>click here to update them all</a>").arg(updateAllLink).arg(newName)
wrapMode: Text.WordWrap
font.pixelSize: Style.subHeadingFontPixelSize
color: "white"
onLinkActivated: {
_launcher.requestUpdateAllAircraft();
dismiss(); // request our dismissal
}
}

View file

@ -1,4 +1,5 @@
import QtQuick 2.4
import QtQml 2.4
import FlightGear 1.0
import "."
@ -149,6 +150,18 @@ Item {
source: "qrc:///qml/Summary.qml"
}
NotificationArea {
id: notifications
// only show on the summary page
visible: sidebar.selectedPage === 0
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
}
function selectPage(index)
{
sidebar.setSelectedPage(index);

View file

@ -0,0 +1,28 @@
import QtQuick 2.4
import FlightGear.Launcher 1.0
import "."
ClickableText {
signal dismiss();
text: msg.arg(_updates.updateVersion)
readonly property string msg: (_updates.status == UpdateChecker.MajorUpdate) ?
qsTr("A new release of FlightGear is available (%1): click for more information")
: qsTr("Updated version %1 is available: click here to download")
wrapMode: Text.WordWrap
font.pixelSize: Style.subHeadingFontPixelSize
color: "white"
onClicked: {
_launcher.launchUrl(_updates.updateUri);
}
function dismissed()
{
_updates.ignoreUpdate();
}
}

View file

@ -0,0 +1,111 @@
import QtQuick 2.4
import QtQml 2.4
import FlightGear.Launcher 1.0
import ".."
Item {
id: root
width: 500
readonly property int ourMargin: Style.margin * 2
Component {
id: notificationBox
Rectangle {
id: boxRoot
property alias content: contentLoader.sourceComponent
width: notificationsColumn.width
height: contentLoader.height + (ourMargin * 2)
clip: true
color: Style.themeColor
border.width: 1
border.color: Qt.darker(Style.themeColor)
Rectangle {
id: background
anchors.fill: parent
z: -1
opacity: Style.panelOpacity
color: "white"
}
Loader {
// height is not anchored, can float
anchors {
top: parent.top
left: parent.left
right: closeButton.left
margins: ourMargin
}
id: contentLoader
source: model.source
// don't set height, comes from content
}
Connections {
target: contentLoader.item
onDismiss: {
_notifications.dismissIndex(model.index)
}
}
Image {
id: closeButton
source: "qrc:///white-delete-icon"
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
margins: ourMargin
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contentLoader.item.dismissed) {
contentLoader.item.dismissed();
}
_notifications.dismissIndex(model.index)
}
}
}
} // of notification box
}
// ensure clicks 'near' the notifications don't go to other UI
MouseArea {
width: parent.width
height: notificationsColumn.height
}
Column {
id: notificationsColumn
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
left: parent.left
margins: Style.strutSize
}
spacing: Style.strutSize
Repeater {
model: _notifications.active
delegate: notificationBox
}
} // of boxes column
}

View file

@ -1,4 +1,5 @@
import QtQuick 2.4
import QtQml 2.4
import FlightGear.Launcher 1.0
import "."
@ -77,54 +78,7 @@ Item {
styleColor: "black"
}
Row {
spacing: Style.margin
anchors {
left: logoText.left
right: logoText.right
}
// anchoring to logoText bottom doesn't work as expected because of
// dynamic text sizing, so bind it manually
y: logoText.y + Style.margin + logoText.contentHeight
id: updateMessage
visible: _updates.status != UpdateChecker.NoUpdate
readonly property string msg: (_updates.status == UpdateChecker.MajorUpdate) ?
qsTr("A new release of FlightGear is available (%1): click for more information")
: qsTr("Updated version %1 is available: click here to download")
ClickableText {
text: parent.msg.arg(_updates.updateVersion)
baseTextColor: "white"
style: Text.Outline
styleColor: "black"
font.bold: true
font.pixelSize: Style.headingFontPixelSize
onClicked: {
_launcher.launchUrl(_updates.updateUri);
}
}
ClickableText {
text: qsTr("(or click to ignore this)")
baseTextColor: "white"
style: Text.Outline
styleColor: "black"
font.bold: true
font.pixelSize: Style.headingFontPixelSize
onClicked: {
_updates.ignoreUpdate();
}
}
}
ClickableText {
visible: !updateMessage.visible
anchors {
left: logoText.left
right: logoText.right
@ -360,5 +314,5 @@ Item {
width: 1; height: 1
}
}
}
} // of summary box
}

View file

@ -110,6 +110,7 @@
<file>qml/LocationAltitudeRow.qml</file>
<file>qml/CatalogDelegate.qml</file>
<file alias="clear-text-icon">assets/icons8-clear-symbol-26.png</file>
<file alias="white-delete-icon">assets/white-cross-icon.png</file>
<file>qml/FlightPlan.qml</file>
<file>qml/PlainTextEditBox.qml</file>
<file>qml/HeaderBox.qml</file>
@ -136,6 +137,11 @@
<file alias="favourite-icon-outline">assets/icons8-christmas-star-outline.png</file>
<file>qml/FirstRun.qml</file>
<file>qml/HelpSupport.qml</file>
<file>qml/NotificationArea.qml</file>
<file>qml/NewVersionNotification.qml</file>
<file>qml/DidMigrateOfficialCatalogNotification.qml</file>
<file>qml/DidMigrateOtherCatalogNotification.qml</file>
</qresource>
<qresource prefix="/preview">
<file alias="close-icon">assets/preview-close.png</file>

View file

@ -124,12 +124,15 @@ bool FGHTTPClient::isDefaultCatalogInstalled() const
return getDefaultCatalog().valid();
}
void FGHTTPClient::addDefaultCatalog()
pkg::CatalogRef FGHTTPClient::addDefaultCatalog()
{
pkg::CatalogRef defaultCatalog = getDefaultCatalog();
if (!defaultCatalog.valid()) {
pkg::Catalog::createFromUrl(globals->packageRoot(), getDefaultCatalogUrl());
auto cat = pkg::Catalog::createFromUrl(globals->packageRoot(), getDefaultCatalogUrl());
return cat;
}
return defaultCatalog;
}
std::string FGHTTPClient::getDefaultCatalogId() const

View file

@ -21,8 +21,10 @@
#ifndef FG_HTTP_CLIENT_HXX
#define FG_HTTP_CLIENT_HXX
#include <simgear/structure/subsystem_mgr.hxx>
#include <simgear/io/HTTPClient.hxx>
#include <simgear/package/Catalog.hxx>
#include <simgear/structure/subsystem_mgr.hxx>
#include <memory>
class FGHTTPClient : public SGSubsystem
@ -46,7 +48,7 @@ public:
simgear::HTTP::Client const* client() const { return _http.get(); }
bool isDefaultCatalogInstalled() const;
void addDefaultCatalog();
simgear::pkg::CatalogRef addDefaultCatalog();
std::string getDefaultCatalogId() const;
std::string getDefaultCatalogUrl() const;