From 9a623129d8d3f4b2eada9fb89ec4760601aded2d Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Wed, 16 Apr 2025 17:13:14 +0200 Subject: [PATCH] Add Notification window to the Launcher (#3595) --- apps/OpenSpace/ext/launcher/CMakeLists.txt | 2 + .../ext/launcher/include/notificationwindow.h | 42 ++++ .../ext/launcher/resources/qss/launcher.qss | 6 + .../ext/launcher/src/launcherwindow.cpp | 30 ++- .../ext/launcher/src/notificationwindow.cpp | 223 ++++++++++++++++++ apps/OpenSpace/main.cpp | 16 ++ include/openspace/engine/settings.h | 4 + src/engine/settings.cpp | 4 + 8 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 apps/OpenSpace/ext/launcher/include/notificationwindow.h create mode 100644 apps/OpenSpace/ext/launcher/src/notificationwindow.cpp diff --git a/apps/OpenSpace/ext/launcher/CMakeLists.txt b/apps/OpenSpace/ext/launcher/CMakeLists.txt index 64f8aa3e70..ee529bcfb3 100644 --- a/apps/OpenSpace/ext/launcher/CMakeLists.txt +++ b/apps/OpenSpace/ext/launcher/CMakeLists.txt @@ -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 diff --git a/apps/OpenSpace/ext/launcher/include/notificationwindow.h b/apps/OpenSpace/ext/launcher/include/notificationwindow.h new file mode 100644 index 0000000000..fd7007c88f --- /dev/null +++ b/apps/OpenSpace/ext/launcher/include/notificationwindow.h @@ -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 + +#include +#include + +class NotificationWindow final : public QTextEdit { +Q_OBJECT +public: + explicit NotificationWindow(QWidget* parent); + +private: + std::unique_ptr _request; +}; + +#endif // __OPENSPACE_UI_LAUNCHER___NOTIFICATIONWINDOW___H__ diff --git a/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss b/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss index b9b1ed9fef..419201ce1e 100644 --- a/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss +++ b/apps/OpenSpace/ext/launcher/resources/qss/launcher.qss @@ -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 */ diff --git a/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp b/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp index 2b9750e089..a3d0930422 100644 --- a/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp +++ b/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp @@ -26,6 +26,7 @@ #include "profile/profileedit.h" #include "backgroundimage.h" +#include "notificationwindow.h" #include "settingsdialog.h" #include "splitcombobox.h" #include @@ -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 // diff --git a/apps/OpenSpace/ext/launcher/src/notificationwindow.cpp b/apps/OpenSpace/ext/launcher/src/notificationwindow.cpp new file mode 100644 index 0000000000..f154b976fd --- /dev/null +++ b/apps/OpenSpace/ext/launcher/src/notificationwindow.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::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 parseEntries(const std::string& data) { + std::vector entries; + + std::vector lines = ghoul::tokenizeString(data, '\n'); + std::vector::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(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( + "" + "" + "{0}" + "" + "" + "{1}" + "" + "", + e.date, e.text, text.red(), text.green(), text.blue() + ); + } + else { + return std::format( + "" + "" + "{0}" + "" + "" + "{1}" + "" + "", + 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(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& data = _request->downloadedData(); + std::string notificationText = std::string(data.begin(), data.end()); + + // 2. Parse the retrieved data into entries + std::vector 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(std::chrono::system_clock::now()) + ); + std::erase_if( + entries, + [now](const Entry& e) { + auto r = scn::scan(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(*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("{}
", std::move(text)); + + // 5. Set the text + QString t = QString::fromStdString(text); + setText(t); + }); +} diff --git a/apps/OpenSpace/main.cpp b/apps/OpenSpace/main.cpp index 118846bb0e..1e9847c213 100644 --- a/apps/OpenSpace/main.cpp +++ b/apps/OpenSpace/main.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -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(std::chrono::system_clock::now()) + ); + settings.lastStartedDate = std::format( + "{}-{:0>2}-{:0>2}", + static_cast(now.year()), + static_cast(now.month()), + static_cast(now.day()) + ); saveSettings(settings, findSettings()); } diff --git a/include/openspace/engine/settings.h b/include/openspace/engine/settings.h index d167d8544d..f0c514ee8b 100644 --- a/include/openspace/engine/settings.h +++ b/include/openspace/engine/settings.h @@ -35,8 +35,12 @@ namespace openspace { struct Settings { auto operator<=>(const Settings&) const = default; + // Settings that are not configurable by the user and that represent a persistent + // state for the system std::optional hasStartedBefore; + std::optional lastStartedDate; + // Configurable settings std::optional configuration; std::optional rememberLastConfiguration; std::optional profile; diff --git a/src/engine/settings.cpp b/src/engine/settings.cpp index 49feff88ad..2bdc562a17 100644 --- a/src/engine/settings.cpp +++ b/src/engine/settings.cpp @@ -49,6 +49,7 @@ namespace version1 { Settings settings; settings.hasStartedBefore = get_to(json, "started-before"); + settings.lastStartedDate = get_to(json, "last-started-date"); settings.configuration = get_to(json, "config"); settings.rememberLastConfiguration = get_to(json, "config-remember"); settings.profile = get_to(json, "profile"); @@ -140,6 +141,9 @@ void saveSettings(const Settings& settings, const std::filesystem::path& filenam if (settings.hasStartedBefore.has_value()) { json["started-before"] = *settings.hasStartedBefore; } + if (settings.lastStartedDate.has_value()) { + json["last-started-date"] = *settings.lastStartedDate; + } if (settings.configuration.has_value()) { json["config"] = *settings.configuration; }