From df42c6f02688237d6b67256e43a0eb3af4dd7a9f Mon Sep 17 00:00:00 2001
From: Automatic Release Builder <build@flightgear.org>
Date: Sun, 25 Oct 2020 18:24:33 +0000
Subject: [PATCH] Launcher: UI feedback for hangar migrations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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.
---
 src/GUI/CMakeLists.txt                        |   4 +
 src/GUI/LauncherMainWindow.cxx                |  33 ++--
 src/GUI/LauncherMainWindow.hxx                |   4 +
 src/GUI/LauncherNotificationsController.cxx   | 167 ++++++++++++++++++
 src/GUI/LauncherNotificationsController.hxx   |  59 +++++++
 src/GUI/LauncherPackageDelegate.cxx           |  73 ++++++++
 src/GUI/LauncherPackageDelegate.hxx           |  37 ++++
 src/GUI/UpdateChecker.cxx                     |   5 +
 src/GUI/assets/white-cross-icon.png           | Bin 0 -> 290 bytes
 .../DidMigrateOfficialCatalogNotification.qml |  21 +++
 .../DidMigrateOtherCatalogNotification.qml    |  21 +++
 src/GUI/qml/Launcher.qml                      |  13 ++
 src/GUI/qml/NewVersionNotification.qml        |  28 +++
 src/GUI/qml/NotificationArea.qml              | 111 ++++++++++++
 src/GUI/qml/Summary.qml                       |  50 +-----
 src/GUI/resources.qrc                         |   6 +
 src/Network/HTTPClient.cxx                    |   7 +-
 src/Network/HTTPClient.hxx                    |   6 +-
 18 files changed, 580 insertions(+), 65 deletions(-)
 create mode 100644 src/GUI/LauncherNotificationsController.cxx
 create mode 100644 src/GUI/LauncherNotificationsController.hxx
 create mode 100644 src/GUI/LauncherPackageDelegate.cxx
 create mode 100644 src/GUI/LauncherPackageDelegate.hxx
 create mode 100644 src/GUI/assets/white-cross-icon.png
 create mode 100644 src/GUI/qml/DidMigrateOfficialCatalogNotification.qml
 create mode 100644 src/GUI/qml/DidMigrateOtherCatalogNotification.qml
 create mode 100644 src/GUI/qml/NewVersionNotification.qml
 create mode 100644 src/GUI/qml/NotificationArea.qml

diff --git a/src/GUI/CMakeLists.txt b/src/GUI/CMakeLists.txt
index ab968b9d4..adbd667b8 100644
--- a/src/GUI/CMakeLists.txt
+++ b/src/GUI/CMakeLists.txt
@@ -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})
diff --git a/src/GUI/LauncherMainWindow.cxx b/src/GUI/LauncherMainWindow.cxx
index 22ed5bcee..6a9b65416 100755
--- a/src/GUI/LauncherMainWindow.cxx
+++ b/src/GUI/LauncherMainWindow.cxx
@@ -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()
 {
diff --git a/src/GUI/LauncherMainWindow.hxx b/src/GUI/LauncherMainWindow.hxx
index b8c8289ad..051fb2cb4 100644
--- a/src/GUI/LauncherMainWindow.hxx
+++ b/src/GUI/LauncherMainWindow.hxx
@@ -48,6 +48,10 @@ public:
     bool wasRejected();
 
     bool event(QEvent *event) override;
+
+private slots:
+    void onQuickStatusChanged(QQuickView::Status status);
+
 private:
     LauncherController* m_controller;
 };
diff --git a/src/GUI/LauncherNotificationsController.cxx b/src/GUI/LauncherNotificationsController.cxx
new file mode 100644
index 000000000..760693437
--- /dev/null
+++ b/src/GUI/LauncherNotificationsController.cxx
@@ -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);
+}
diff --git a/src/GUI/LauncherNotificationsController.hxx b/src/GUI/LauncherNotificationsController.hxx
new file mode 100644
index 000000000..63180942b
--- /dev/null
+++ b/src/GUI/LauncherNotificationsController.hxx
@@ -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;
+};
diff --git a/src/GUI/LauncherPackageDelegate.cxx b/src/GUI/LauncherPackageDelegate.cxx
new file mode 100644
index 000000000..d7fa815d2
--- /dev/null
+++ b/src/GUI/LauncherPackageDelegate.cxx
@@ -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)
+}
diff --git a/src/GUI/LauncherPackageDelegate.hxx b/src/GUI/LauncherPackageDelegate.hxx
new file mode 100644
index 000000000..66a4c61e4
--- /dev/null
+++ b/src/GUI/LauncherPackageDelegate.hxx
@@ -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
diff --git a/src/GUI/UpdateChecker.cxx b/src/GUI/UpdateChecker.cxx
index 35414df8e..9fe769f80 100644
--- a/src/GUI/UpdateChecker.cxx
+++ b/src/GUI/UpdateChecker.cxx
@@ -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) {
diff --git a/src/GUI/assets/white-cross-icon.png b/src/GUI/assets/white-cross-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..db6b5f7156e5a9ccd312db96276d782064c784ba
GIT binary patch
literal 290
zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3Y)RhkE)4%caKYZ?lYt`FJzX3_
zJUZV_yUW`Yz|-1o?NPwKJt0r+fVyEK`}7|*nM#V{FD9y~o${<$Xj`-W>A#t>zq}bu
z3qCKu_wfDUx(7UWDzjPin7$bA?=M-WXjHVmMB~Tr%6FO(C$65^WxC_I)%mNT8COG&
zZez9awX)$|b>Q+s;UAY$4&GmX^hxT9ka^KB`6}c>8oV#Ou<Bdo^>@~dS0BF3Vm;gZ
zx%cUoz?U2*jWTOLafF=cJt&%?HRn?$kM#fEuRlM0to&oJdE2U0&x4wdM}cgZy+!%$
mr|R2PQ%*-M`hOw+1*`jO_NPnIL>2<Q$l&Sf=d#Wzp$P!!3x)*%

literal 0
HcmV?d00001

diff --git a/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml b/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml
new file mode 100644
index 000000000..09d240174
--- /dev/null
+++ b/src/GUI/qml/DidMigrateOfficialCatalogNotification.qml
@@ -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
+    }
+}
diff --git a/src/GUI/qml/DidMigrateOtherCatalogNotification.qml b/src/GUI/qml/DidMigrateOtherCatalogNotification.qml
new file mode 100644
index 000000000..6f0b8be44
--- /dev/null
+++ b/src/GUI/qml/DidMigrateOtherCatalogNotification.qml
@@ -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
+    }
+}
diff --git a/src/GUI/qml/Launcher.qml b/src/GUI/qml/Launcher.qml
index 748961837..ddfe07282 100644
--- a/src/GUI/qml/Launcher.qml
+++ b/src/GUI/qml/Launcher.qml
@@ -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);
diff --git a/src/GUI/qml/NewVersionNotification.qml b/src/GUI/qml/NewVersionNotification.qml
new file mode 100644
index 000000000..b997f6ce1
--- /dev/null
+++ b/src/GUI/qml/NewVersionNotification.qml
@@ -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();
+    }
+}
+
diff --git a/src/GUI/qml/NotificationArea.qml b/src/GUI/qml/NotificationArea.qml
new file mode 100644
index 000000000..ebaf8dcbb
--- /dev/null
+++ b/src/GUI/qml/NotificationArea.qml
@@ -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
+
+}
diff --git a/src/GUI/qml/Summary.qml b/src/GUI/qml/Summary.qml
index 7445bc781..22f7673b5 100644
--- a/src/GUI/qml/Summary.qml
+++ b/src/GUI/qml/Summary.qml
@@ -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
 }
diff --git a/src/GUI/resources.qrc b/src/GUI/resources.qrc
index 107fff5d4..75c88a154 100644
--- a/src/GUI/resources.qrc
+++ b/src/GUI/resources.qrc
@@ -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>
diff --git a/src/Network/HTTPClient.cxx b/src/Network/HTTPClient.cxx
index 1015c9169..f06b4c8f3 100644
--- a/src/Network/HTTPClient.cxx
+++ b/src/Network/HTTPClient.cxx
@@ -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
diff --git a/src/Network/HTTPClient.hxx b/src/Network/HTTPClient.hxx
index 640fff7b3..07be3a452 100644
--- a/src/Network/HTTPClient.hxx
+++ b/src/Network/HTTPClient.hxx
@@ -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;