Add Notification window to the Launcher (#3595)

This commit is contained in:
Alexander Bock
2025-04-16 17:13:14 +02:00
committed by GitHub
parent ba392d97ea
commit 9a623129d8
8 changed files with 320 additions and 7 deletions

View File

@@ -28,6 +28,7 @@ set(HEADER_FILES
include/backgroundimage.h
include/filesystemaccess.h
include/launcherwindow.h
include/notificationwindow.h
include/settingsdialog.h
include/splitcombobox.h
include/windowcolors.h
@@ -59,6 +60,7 @@ set(SOURCE_FILES
src/backgroundimage.cpp
src/launcherwindow.cpp
src/filesystemaccess.cpp
src/notificationwindow.cpp
src/settingsdialog.cpp
src/splitcombobox.cpp
src/windowcolors.cpp

View File

@@ -0,0 +1,42 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_UI_LAUNCHER___NOTIFICATIONWINDOW___H__
#define __OPENSPACE_UI_LAUNCHER___NOTIFICATIONWINDOW___H__
#include <QTextEdit>
#include <openspace/util/httprequest.h>
#include <memory>
class NotificationWindow final : public QTextEdit {
Q_OBJECT
public:
explicit NotificationWindow(QWidget* parent);
private:
std::unique_ptr<openspace::HttpMemoryDownload> _request;
};
#endif // __OPENSPACE_UI_LAUNCHER___NOTIFICATIONWINDOW___H__

View File

@@ -119,6 +119,12 @@ LauncherWindow QComboBox#config:focus
LauncherWindow QPushButton#settings:focus {
outline: 2px solid rgb(61, 189, 238);
}
LauncherWindow QTextEdit#notifications {
background-color: #242424;
color: #d7d7d7;
}
/*
* ProfileEdit
*/

View File

@@ -26,6 +26,7 @@
#include "profile/profileedit.h"
#include "backgroundimage.h"
#include "notificationwindow.h"
#include "settingsdialog.h"
#include "splitcombobox.h"
#include <openspace/openspace.h>
@@ -45,8 +46,10 @@
using namespace openspace;
namespace {
constexpr int ScreenWidth = 480;
constexpr int ScreenHeight = 640;
constexpr int MainScreenWidth = 480;
constexpr int MainScreenHeight = 640;
constexpr int FullScreenWidth = MainScreenWidth;
constexpr int FullScreenHeight = 706;
constexpr int LeftRuler = 40;
constexpr int TopRuler = 80;
@@ -55,10 +58,12 @@ namespace {
constexpr int SmallItemWidth = 100;
constexpr int SmallItemHeight = SmallItemWidth / 4;
constexpr int NotificationShelfHeight = FullScreenHeight - MainScreenHeight;
constexpr int SettingsIconSize = 35;
namespace geometry {
constexpr QRect BackgroundImage(0, 0, ScreenWidth, ScreenHeight);
constexpr QRect BackgroundImage(0, 0, MainScreenWidth, MainScreenHeight);
constexpr QRect LogoImage(LeftRuler, TopRuler, ItemWidth, ItemHeight);
constexpr QRect ChooseLabel(LeftRuler + 10, TopRuler + 80, 151, 24);
constexpr QRect ProfileBox(LeftRuler, TopRuler + 110, ItemWidth, ItemHeight);
@@ -80,14 +85,19 @@ namespace {
LeftRuler, TopRuler + 400, ItemWidth, ItemHeight
);
constexpr QRect VersionString(
5, ScreenHeight - SmallItemHeight, ItemWidth, SmallItemHeight
5, MainScreenHeight - SmallItemHeight, ItemWidth, SmallItemHeight
);
constexpr QRect SettingsButton(
ScreenWidth - SettingsIconSize - 5,
ScreenHeight - SettingsIconSize - 5,
MainScreenWidth - SettingsIconSize - 5,
MainScreenHeight - SettingsIconSize - 5,
SettingsIconSize,
SettingsIconSize
);
constexpr QRect NotificationShelf(
0,
MainScreenHeight,
MainScreenWidth,
NotificationShelfHeight);
} // namespace geometry
@@ -132,7 +142,7 @@ LauncherWindow::LauncherWindow(bool profileEnabled, const Configuration& globalC
);
setWindowTitle("OpenSpace Launcher");
setFixedSize(ScreenWidth, ScreenHeight);
setFixedSize(FullScreenWidth, FullScreenHeight);
setAutoFillBackground(false);
{
@@ -163,6 +173,12 @@ LauncherWindow::LauncherWindow(bool profileEnabled, const Configuration& globalC
logoImage->setPixmap(QPixmap(":/images/openspace-horiz-logo-small.png"));
}
{
NotificationWindow* notificationWindow = new NotificationWindow(centralWidget);
notificationWindow->setGeometry(geometry::NotificationShelf);
notificationWindow->show();
}
//
// Profile chooser
//

View File

@@ -0,0 +1,223 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "notificationwindow.h"
#include <openspace/openspace.h>
#include <openspace/engine/settings.h>
#include <openspace/util/httprequest.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/assert.h>
#include <ghoul/misc/stringhelper.h>
#include <QGuiApplication>
#include <QStyleHints>
#include <QTimer>
#include <scn/scan.h>
#include <date/date.h>
#include <string_view>
#include <vector>
using namespace openspace;
namespace {
struct Entry {
std::string date;
std::string text;
};
// Parses a single notification entry out of the list of lines
Entry parseEntry(std::vector<std::string>::const_iterator& curr) {
ghoul_assert(!curr->empty(), "First line must not be empty");
std::string date = *curr;
std::string text;
do {
curr++;
text += *curr;
} while (!curr->empty());
curr++;
return { std::move(date), std::move(text) };
}
std::vector<Entry> parseEntries(const std::string& data) {
std::vector<Entry> entries;
std::vector<std::string> lines = ghoul::tokenizeString(data, '\n');
std::vector<std::string>::const_iterator curr = lines.cbegin();
while (curr != lines.end()) {
Entry e = parseEntry(curr);
entries.push_back(std::move(e));
}
return entries;
}
std::string formatEntry(const Entry& e, date::year_month_day lastStartedDate) {
auto r = scn::scan<int, int, int>(e.date, "{}-{}-{}");
ghoul_assert(r, "Invalid date");
auto& [year, month, day] = r->values();
const date::year_month_day ymd = date::year_month_day(
date::year(year),
date::month(month),
date::day(day)
);
QColor text = QGuiApplication::palette().text().color();
text = text.darker();
if (date::sys_days(ymd) < date::sys_days(lastStartedDate)) {
QColor text = QColor(120, 120, 120);
return std::format(
"<tr>"
"<td width='15%'>"
"<font color='#{2:x}{3:x}{4:x}'>{0}</font>"
"</td>"
"<td width='85%' align='left'>"
"<font color='#{2:x}{3:x}{4:x}'>{1}</font>"
"</td>"
"</tr>",
e.date, e.text, text.red(), text.green(), text.blue()
);
}
else {
return std::format(
"<tr>"
"<td width='15%'>"
"{0}"
"</td>"
"<td width='85%' align='left'>"
"{1}"
"</td>"
"</tr>",
e.date, e.text
);
}
}
} // namespace
NotificationWindow::NotificationWindow(QWidget* parent)
: QTextEdit(parent)
{
setAcceptRichText(true);
setReadOnly(true);
setFocusPolicy(Qt::NoFocus);
setObjectName("notifications");
std::string URL = std::format(
"https://raw.githubusercontent.com/OpenSpace/Notifications/refs/heads/master/"
"{}.txt",
OPENSPACE_IS_RELEASE_BUILD ? OPENSPACE_VERSION_NUMBER : "master"
);
_request = std::make_unique<HttpMemoryDownload>(std::string(URL));
_request->start(std::chrono::seconds(1));
// The download has a timeout of 1s, so after 1250ms we'll definitely have answer.
constexpr int TimeOut = 1250;
QTimer::singleShot(TimeOut, [this](){
while (!_request->hasSucceeded() && !_request->hasFailed()) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
if (_request->hasFailed()) {
LWARNINGC("Notification", "Failed to retrieve notification file");
// The download has failed for some reason
return;
}
// 1. Get the downloaded data
const std::vector<char>& data = _request->downloadedData();
std::string notificationText = std::string(data.begin(), data.end());
// 2. Parse the retrieved data into entries
std::vector<Entry> entries = parseEntries(notificationText);
// 3. Filter the entries to not show anything that is older than 6 months
const date::year_month_day now = date::year_month_day(
floor<date::days>(std::chrono::system_clock::now())
);
std::erase_if(
entries,
[now](const Entry& e) {
auto r = scn::scan<int, int, int>(e.date, "{}-{}-{}");
if (!r) {
return false;
}
auto& [year, month, day] = r->values();
const date::year_month_day ymd = date::year_month_day(
date::year(year),
date::month(month),
date::day(day)
);
const std::chrono::days diff = date::sys_days(now) - date::sys_days(ymd);
const bool older = diff.count() > (365 / 2);
return older;
}
);
// 4. Format the entries into a table format
Settings settings = loadSettings();
// Picking a date as the default date that is far enough in the past
date::year_month_day lastStart = date::year_month_day(
date::year(2000),
date::month(1),
date::day(1)
);
if (settings.lastStartedDate.has_value()) {
auto r = scn::scan<int, int, int>(*settings.lastStartedDate, "{}-{}-{}");
if (r) {
auto& [year, month, day] = r->values();
lastStart = date::year_month_day(
date::year(year),
date::month(month),
date::day(day)
);
}
}
std::string text = std::accumulate(
entries.begin(),
entries.end(),
std::string(),
[&lastStart](std::string t, const Entry& e) {
return std::format(
"{}{}",
std::move(t), formatEntry(e, lastStart)
);
}
);
// Add the HTML-like table attributes
text = std::format("<table border='0'>{}</table>", std::move(text));
// 5. Set the text
QString t = QString::fromStdString(text);
setText(t);
});
}

View File

@@ -53,6 +53,7 @@
#include <sgct/projection/nonlinearprojection.h>
#include <sgct/user.h>
#include <sgct/window.h>
#include <date/date.h>
#include <stb_image.h>
#include <tracy/Tracy.hpp>
#include <iostream>
@@ -1086,6 +1087,12 @@ void setSgctDelegateFunctions() {
int main(int argc, char* argv[]) {
ZoneScoped;
// For debugging purposes: Enforce Light Mode in Qt
// qputenv("QT_QPA_PLATFORM", "windows:darkmode=0");
// For debugging purposes: Enforce Dark Mode in Qt
// qputenv("QT_QPA_PLATFORM", "windows:darkmode=1");
#ifdef OPENSPACE_BREAK_ON_FLOATING_POINT_EXCEPTION
_clearfp();
_controlfp(_controlfp(0, 0) & ~(_EM_ZERODIVIDE | _EM_OVERFLOW), _MCW_EM);
@@ -1467,6 +1474,15 @@ int main(int argc, char* argv[]) {
settings.configuration =
isGeneratedWindowConfig ? "" : global::configuration->windowConfiguration;
const date::year_month_day now = date::year_month_day(
floor<date::days>(std::chrono::system_clock::now())
);
settings.lastStartedDate = std::format(
"{}-{:0>2}-{:0>2}",
static_cast<int>(now.year()),
static_cast<unsigned>(now.month()),
static_cast<unsigned>(now.day())
);
saveSettings(settings, findSettings());
}