diff --git a/forms/updatepromoter.ui b/forms/updatepromoter.ui
new file mode 100644
index 00000000..cbb4e8be
--- /dev/null
+++ b/forms/updatepromoter.ui
@@ -0,0 +1,104 @@
+
+
+ UpdatePromoter
+
+
+
+ 0
+ 0
+ 592
+ 484
+
+
+
+ Porymap Version Update
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
-
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-size:13pt; color:#d7000c;">WARNING: </span><span style=" font-weight:400;">Updating Porymap may require you to update your projects. See "Breaking Changes" in the Changelog for details.</span></p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Plain
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Do not alert me about new updates
+
+
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Close|QDialogButtonBox::Retry
+
+
+
+
+
+
+
+
diff --git a/include/mainwindow.h b/include/mainwindow.h
index 75c38fc9..c499a5ba 100644
--- a/include/mainwindow.h
+++ b/include/mainwindow.h
@@ -28,6 +28,7 @@
#include "preferenceeditor.h"
#include "projectsettingseditor.h"
#include "customscriptseditor.h"
+#include "updatepromoter.h"
@@ -298,7 +299,6 @@ private slots:
public:
Ui::MainWindow *ui;
Editor *editor = nullptr;
- QPointer networkAccessManager = nullptr;
private:
QLabel *label_MapRulerStatus = nullptr;
@@ -310,6 +310,8 @@ private:
QPointer preferenceEditor = nullptr;
QPointer projectSettingsEditor = nullptr;
QPointer customScriptsEditor = nullptr;
+ QPointer updatePromoter = nullptr;
+ QPointer networkAccessManager = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
QStandardItemModel *mapListModel;
QList *mapGroupItemsList;
diff --git a/include/ui/updatepromoter.h b/include/ui/updatepromoter.h
new file mode 100644
index 00000000..4b0f209d
--- /dev/null
+++ b/include/ui/updatepromoter.h
@@ -0,0 +1,44 @@
+#ifndef UPDATEPROMOTER_H
+#define UPDATEPROMOTER_H
+
+#include
+#include
+#include
+#include
+
+namespace Ui {
+class UpdatePromoter;
+}
+
+class UpdatePromoter : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager);
+ ~UpdatePromoter() {};
+
+ void checkForUpdates();
+ void requestDialog();
+ void updatePreferences();
+
+private:
+ Ui::UpdatePromoter *ui;
+ QNetworkAccessManager *const manager;
+ QNetworkReply * reply = nullptr;
+ QPushButton * button_Downloads;
+ QString downloadLink;
+ QString changelog;
+
+ void resetDialog();
+ void processWebpage(const QJsonDocument &data);
+ void processError(const QString &err);
+
+private slots:
+ void dialogButtonClicked(QAbstractButton *button);
+
+signals:
+ void changedPreferences();
+};
+
+#endif // UPDATEPROMOTER_H
diff --git a/porymap.pro b/porymap.pro
index f5cb0452..4e883b39 100644
--- a/porymap.pro
+++ b/porymap.pro
@@ -110,7 +110,8 @@ SOURCES += src/core/block.cpp \
src/project.cpp \
src/settings.cpp \
src/log.cpp \
- src/ui/uintspinbox.cpp
+ src/ui/uintspinbox.cpp \
+ src/ui/updatepromoter.cpp
HEADERS += include/core/block.h \
include/core/bitpacker.h \
@@ -204,7 +205,8 @@ HEADERS += include/core/block.h \
include/scriptutility.h \
include/settings.h \
include/log.h \
- include/ui/uintspinbox.h
+ include/ui/uintspinbox.h \
+ include/ui/updatepromoter.h
FORMS += forms/mainwindow.ui \
forms/prefabcreationdialog.ui \
@@ -222,7 +224,8 @@ FORMS += forms/mainwindow.ui \
forms/colorpicker.ui \
forms/projectsettingseditor.ui \
forms/customscriptseditor.ui \
- forms/customscriptslistitem.ui
+ forms/customscriptslistitem.ui \
+ forms/updatepromoter.ui
RESOURCES += \
resources/images.qrc \
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index f98b3a30..9fd45720 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -248,9 +248,6 @@ void MainWindow::initExtraSignals() {
label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse);
}
-// TODO: Relocate
-#include
-#include
void MainWindow::on_actionCheck_for_Updates_triggered() {
checkForUpdates(true);
}
@@ -259,63 +256,17 @@ void MainWindow::checkForUpdates(bool requestedByUser) {
if (!this->networkAccessManager)
this->networkAccessManager = new QNetworkAccessManager(this);
- // We could get ".../releases/latest" to retrieve less data, but this would run into problems if the
- // most recent item on the releases page is not actually a new release (like the static windows build).
- static const QUrl url("https://api.github.com/repos/huderlem/porymap/releases");
- QNetworkRequest request(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-
- QNetworkReply * reply = this->networkAccessManager->get(request);
-
- if (requestedByUser) {
- // TODO: Show dialog
+ if (!this->updatePromoter) {
+ this->updatePromoter = new UpdatePromoter(this, this->networkAccessManager);
+ connect(this->updatePromoter, &UpdatePromoter::changedPreferences, [this] {
+ if (this->preferenceEditor)
+ this->preferenceEditor->updateFields();
+ });
}
- connect(reply, &QNetworkReply::finished, [this, reply] {
- QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
- QJsonArray releases = data.array();
-
- // Read all the items on the releases page, stopping when we find a tag that parses as a version identifier.
- // Objects in the releases page data are sorted newest to oldest. Although I can't find a guarantee of this in
- // GitHub's API documentation, this seems unlikely to change.
- for (int i = 0; i < releases.size(); i++) {
- auto release = releases.at(i).toObject();
-
- const QStringList tag = release.value("tag_name").toString().split(".");
- if (tag.length() != 3) continue;
-
- bool ok;
- int major = tag.at(0).toInt(&ok);
- if (!ok) continue;
- int minor = tag.at(1).toInt(&ok);
- if (!ok) continue;
- int patch = tag.at(2).toInt(&ok);
- if (!ok) continue;
-
- const QString downloadLink = release.value("html_url").toString();
- if (downloadLink.isEmpty()) continue;
-
- // We've found a valid release tag, we can stop reading.
- logInfo(QString("Newest release is %1.%2.%3\n%4").arg(major).arg(minor).arg(patch).arg(downloadLink));
-
- // If the release was published very recently it won't have a description yet, in which case don't tell the user.
- const QString changelog = release.value("body").toString();
- if (changelog.isEmpty()) break;
-
- bool newVersionAvailable;
- if (major != PORYMAP_VERSION_MAJOR) {
- newVersionAvailable = major > PORYMAP_VERSION_MAJOR;
- } else if (minor != PORYMAP_VERSION_MINOR) {
- newVersionAvailable = minor > PORYMAP_VERSION_MINOR;
- } else {
- newVersionAvailable = patch > PORYMAP_VERSION_PATCH;
- }
- logInfo(QString("Host version is %1").arg(newVersionAvailable ? "old" : "up to date"));
-
- // TODO: Show appropriate dialog
- break;
- }
- });
+ if (requestedByUser)
+ this->updatePromoter->requestDialog();
+ this->updatePromoter->checkForUpdates();
}
void MainWindow::initEditor() {
@@ -2812,6 +2763,9 @@ void MainWindow::togglePreferenceSpecificUi() {
ui->actionOpen_Project_in_Text_Editor->setEnabled(false);
else
ui->actionOpen_Project_in_Text_Editor->setEnabled(true);
+
+ if (this->updatePromoter)
+ this->updatePromoter->updatePreferences();
}
void MainWindow::openProjectSettingsEditor(int tab) {
diff --git a/src/ui/updatepromoter.cpp b/src/ui/updatepromoter.cpp
new file mode 100644
index 00000000..c7dc9de7
--- /dev/null
+++ b/src/ui/updatepromoter.cpp
@@ -0,0 +1,177 @@
+#include "updatepromoter.h"
+#include "ui_updatepromoter.h"
+#include "log.h"
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
+ : QDialog(parent),
+ ui(new Ui::UpdatePromoter),
+ manager(manager)
+{
+ ui->setupUi(this);
+
+ // Set up "Do not alert me" check box
+ this->updatePreferences();
+ connect(ui->checkBox_StopAlerts, &QCheckBox::stateChanged, [this](int state) {
+ bool enable = (state != Qt::Checked);
+ porymapConfig.setCheckForUpdates(enable);
+ emit this->changedPreferences();
+ });
+
+ // Set up button box
+ this->button_Downloads = ui->buttonBox->addButton("Go to Downloads...", QDialogButtonBox::ActionRole);
+ connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &UpdatePromoter::dialogButtonClicked);
+
+ this->resetDialog();
+}
+
+void UpdatePromoter::resetDialog() {
+ this->button_Downloads->setEnabled(false);
+ ui->text_Changelog->setVisible(false);
+ ui->label_Warning->setVisible(false);
+ ui->label_Status->setText("Checking for updates...");
+
+ this->changelog = QString();
+ this->downloadLink = QString();
+}
+
+void UpdatePromoter::checkForUpdates() {
+ // Ignore request if one is still active.
+ if (this->reply && !this->reply->isFinished())
+ return;
+ this->resetDialog();
+ ui->buttonBox->button(QDialogButtonBox::Retry)->setEnabled(false);
+
+ // We could get ".../releases/latest" to retrieve less data, but this would run into problems if the
+ // most recent item on the releases page is not actually a new release (like the static windows build).
+ // By getting all releases we can also present a multi-version changelog of all changes since the host release.
+ static const QNetworkRequest request(QUrl("https://api.github.com/repos/huderlem/porymap/releases"));
+ this->reply = this->manager->get(request);
+
+ connect(this->reply, &QNetworkReply::finished, [this] {
+ ui->buttonBox->button(QDialogButtonBox::Retry)->setEnabled(true);
+ auto error = this->reply->error();
+ if (error == QNetworkReply::NoError) {
+ this->processWebpage(QJsonDocument::fromJson(this->reply->readAll()));
+ } else {
+ this->processError(this->reply->errorString());
+ }
+ });
+}
+
+// Read all the items on the releases page, ignoring entries without a version identifier tag.
+// Objects in the releases page data are sorted newest to oldest.
+void UpdatePromoter::processWebpage(const QJsonDocument &data) {
+ bool updateAvailable = false;
+ bool breakingChanges = false;
+ bool foundRelease = false;
+
+ const QJsonArray releases = data.array();
+ for (int i = 0; i < releases.size(); i++) {
+ auto release = releases.at(i).toObject();
+
+ // Convert tag string to version numbers
+ const QString tagName = release.value("tag_name").toString();
+ const QStringList tag = tagName.split(".");
+ if (tag.length() != 3) continue;
+ bool ok;
+ int major = tag.at(0).toInt(&ok);
+ if (!ok) continue;
+ int minor = tag.at(1).toInt(&ok);
+ if (!ok) continue;
+ int patch = tag.at(2).toInt(&ok);
+ if (!ok) continue;
+
+ // We've found a valid release tag. If the version number is not newer than the host version then we can stop looking at releases.
+ foundRelease = true;
+ logInfo(QString("Found release %1.%2.%3").arg(major).arg(minor).arg(patch)); // TODO: Remove
+ if (major <= PORYMAP_VERSION_MAJOR && minor <= PORYMAP_VERSION_MINOR && patch <= PORYMAP_VERSION_PATCH)
+ break;
+
+ const QString description = release.value("body").toString();
+ const QString url = release.value("html_url").toString();
+ if (description.isEmpty() || url.isEmpty()) {
+ // If the release was published very recently it won't have a description yet, in which case don't tell the user about it yet.
+ // If there's no URL, something has gone wrong and we should skip this release.
+ continue;
+ }
+
+ if (this->downloadLink.isEmpty()) {
+ // This is the first (newest) release we've found. Record its URL for download.
+ this->downloadLink = url;
+ breakingChanges = (major > PORYMAP_VERSION_MAJOR);
+ }
+
+ // Record the changelog of this release so we can show all changes since the host release.
+ this->changelog.append(QString("## %1\n%2\n\n").arg(tagName).arg(description));
+ updateAvailable = true;
+ }
+
+ if (!foundRelease) {
+ // We retrieved the webpage but didn't successfully parse any releases.
+ this->processError("Error parsing releases webpage");
+ return;
+ }
+
+ // If there's a new update available the dialog will always be opened.
+ // Otherwise the dialog is only open if the user requested it.
+ if (updateAvailable) {
+ this->button_Downloads->setEnabled(!this->downloadLink.isEmpty());
+ ui->text_Changelog->setMarkdown(this->changelog);
+ ui->text_Changelog->setVisible(true);
+ ui->label_Warning->setVisible(breakingChanges);
+ ui->label_Status->setText("A new version of Porymap is available!");
+ this->show();
+ } else {
+ // The rest of the UI remains in the state set by resetDialog
+ ui->label_Status->setText("Your version of Porymap is up to date!");
+ }
+}
+
+void UpdatePromoter::processError(const QString &err) {
+ const QString message = QString("Failed to check for version update: %1").arg(err);
+ if (this->isVisible()) {
+ ui->label_Status->setText(message);
+ } else {
+ logWarn(message);
+ }
+}
+
+// The dialog can either be shown programmatically when an update is available
+// or if the user manually selects "Check for Updates" in the menu.
+// When the dialog is shown programmatically there is a check box to disable automatic alerts.
+// If the user requested the dialog (and it wasn't already open) this check box should be hidden.
+void UpdatePromoter::requestDialog() {
+ if (!this->isVisible()){
+ ui->checkBox_StopAlerts->setVisible(false);
+ this->show();
+ } else if (this->isMinimized()) {
+ this->showNormal();
+ } else {
+ this->raise();
+ this->activateWindow();
+ }
+}
+
+void UpdatePromoter::updatePreferences() {
+ const QSignalBlocker blocker(ui->checkBox_StopAlerts);
+ ui->checkBox_StopAlerts->setChecked(porymapConfig.getCheckForUpdates());
+}
+
+void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) {
+ auto buttonRole = ui->buttonBox->buttonRole(button);
+ if (buttonRole == QDialogButtonBox::RejectRole) {
+ this->close();
+ } else if (buttonRole == QDialogButtonBox::AcceptRole) {
+ // "Retry" button
+ this->checkForUpdates();
+ } else if (button == this->button_Downloads && !this->downloadLink.isEmpty()) {
+ QDesktopServices::openUrl(QUrl(this->downloadLink));
+ }
+}