Add a new panel to the Launcher to control which WebUI button appears on the bottom bar (#3569)

- Add markNodes to the profiletopic

---------

Co-authored-by: Ylva Selling <ylva.selling@gmail.com>
This commit is contained in:
Alexander Bock
2025-04-03 18:09:35 +02:00
committed by GitHub
parent eb709b830c
commit 6fa3f76c48
14 changed files with 517 additions and 12 deletions

View File

@@ -48,6 +48,7 @@ set(HEADER_FILES
include/profile/timedialog.h
include/profile/profileedit.h
include/profile/propertiesdialog.h
include/profile/uipanelsdialog.h
include/sgctedit/displaywindowunion.h
include/sgctedit/monitorbox.h
include/sgctedit/sgctedit.h
@@ -78,6 +79,7 @@ set(SOURCE_FILES
src/profile/timedialog.cpp
src/profile/profileedit.cpp
src/profile/propertiesdialog.cpp
src/profile/uipanelsdialog.cpp
src/sgctedit/sgctedit.cpp
src/sgctedit/displaywindowunion.cpp
src/sgctedit/monitorbox.cpp

View File

@@ -86,16 +86,17 @@ signals:
void raiseExitWindow();
private slots:
void openMeta();
void openProperties();
void openModules();
void openKeybindings();
void openAssets();
void openTime();
void openAddedScripts();
void openKeybindings();
void openMeta();
void openMarkNodes();
void openDeltaTimes();
void openCamera();
void openMarkNodes();
void openTime();
void openModules();
void openUiPanels();
void openAddedScripts();
void approved();
private:
@@ -112,18 +113,19 @@ private:
std::string _profileFilename;
QLineEdit* _profileEdit = nullptr;
QLabel* _modulesLabel = nullptr;
QLabel* _assetsLabel = nullptr;
QTextEdit* _assetsEdit = nullptr;
QLabel* _propertiesLabel = nullptr;
QTextEdit* _propertiesEdit = nullptr;
QLabel* _assetsLabel = nullptr;
QTextEdit* _assetsEdit = nullptr;
QLabel* _keybindingsLabel = nullptr;
QTextEdit* _keybindingsEdit = nullptr;
QLabel* _deltaTimesLabel = nullptr;
QLabel* _metaLabel = nullptr;
QLabel* _interestingNodesLabel = nullptr;
QLabel* _deltaTimesLabel = nullptr;
QLabel* _cameraLabel = nullptr;
QLabel* _timeLabel = nullptr;
QLabel* _metaLabel = nullptr;
QLabel* _modulesLabel = nullptr;
QLabel* _uiPanelVisibilityLabel = nullptr;
QLabel* _additionalScriptsLabel = nullptr;
};

View File

@@ -0,0 +1,51 @@
/*****************************************************************************************
* *
* 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___UIPANELSDIALOG___H__
#define __OPENSPACE_UI_LAUNCHER___UIPANELSDIALOG___H__
#include <QDialog>
class QCheckBox;
class UiPanelsDialog final : public QDialog {
Q_OBJECT
public:
/**
* Constructor for UiPanelsDialog class.
*
* \param parent Pointer to parent Qt widget
* \param uiPanels The list of ui panels and their visibility
*/
UiPanelsDialog(QWidget* parent, std::map<std::string, bool>* uiPanels);
private slots:
void parseSelections();
private:
std::map<std::string, bool>* _uiPanels;
std::map<QCheckBox*, std::string> _checkboxToId;
};
#endif // __OPENSPACE_UI_LAUNCHER___UIPANELSDIALOG___H__

View File

@@ -35,6 +35,7 @@
#include "profile/modulesdialog.h"
#include "profile/propertiesdialog.h"
#include "profile/timedialog.h"
#include "profile/uipanelsdialog.h"
#include <openspace/scene/profile.h>
#include <ghoul/format.h>
#include <QDialogButtonBox>
@@ -305,6 +306,21 @@ void ProfileEdit::createWidgets() {
rightLayout->addLayout(container);
}
rightLayout->addWidget(new Line);
{
QBoxLayout* container = new QVBoxLayout;
_uiPanelVisibilityLabel = new QLabel("User Interface Panels");
_uiPanelVisibilityLabel->setObjectName("heading");
_uiPanelVisibilityLabel->setWordWrap(true);
container->addWidget(_uiPanelVisibilityLabel);
QPushButton* uiPanelEdit = new QPushButton("Edit");
connect(uiPanelEdit, &QPushButton::clicked, this, &ProfileEdit::openUiPanels);
uiPanelEdit->setLayoutDirection(Qt::RightToLeft);
uiPanelEdit->setAccessibleName("Edit user interface panels");
container->addWidget(uiPanelEdit);
rightLayout->addLayout(container);
}
rightLayout->addWidget(new Line);
{
QBoxLayout* container = new QVBoxLayout;
_additionalScriptsLabel = new QLabel("Additional Scripts");
@@ -375,6 +391,10 @@ void ProfileEdit::openModules() {
_modulesLabel->setText(labelText(_profile.modules.size(), "Modules"));
}
void ProfileEdit::openUiPanels() {
UiPanelsDialog(this, &_profile.uiPanelVisibility).exec();
}
void ProfileEdit::openProperties() {
PropertiesDialog(this, &_profile.properties).exec();
_propertiesLabel->setText(labelText(_profile.properties.size(), "Properties"));

View File

@@ -0,0 +1,130 @@
/*****************************************************************************************
* *
* 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 "profile/uipanelsdialog.h"
#include "profile/line.h"
#include <openspace/json.h>
#include <ghoul/filesystem/filesystem.h>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
#include <fstream>
#include <string_view>
namespace {
constexpr std::string_view DefaultPanelPath = "${DATA}/web/default_ui_panels.json";
struct Panel {
std::string id;
std::string name;
bool isVisible;
};
void from_json(const nlohmann::json& j, Panel& layout) {
j["id"].get_to(layout.id);
j["name"].get_to(layout.name);
j["visible"].get_to(layout.isVisible);
}
std::vector<Panel> loadPanels() {
std::ifstream panelFile = std::ifstream(absPath(DefaultPanelPath));
const std::string panelContent = std::string(
std::istreambuf_iterator<char>(panelFile),
std::istreambuf_iterator<char>()
);
const nlohmann::json panel = nlohmann::json::parse(panelContent);
std::map<std::string, Panel> panels = panel.get<std::map<std::string, Panel>>();
std::vector<Panel> result;
for (const auto& [key, value] : panels) {
result.push_back(value);
}
std::sort(
result.begin(),
result.end(),
[](const Panel& lhs, const Panel& rhs) { return lhs.name < rhs.name; }
);
return result;
}
} // namespace
UiPanelsDialog::UiPanelsDialog(QWidget* parent, std::map<std::string, bool>* uiPanels)
: QDialog(parent)
, _uiPanels(uiPanels)
{
setWindowTitle("User Interface Panels");
std::vector<Panel> panels = loadPanels();
QBoxLayout* layout = new QVBoxLayout(this);
QLabel* info = new QLabel(
"Select the user interface panels that should be visible by default in the "
"current profile."
);
info->setWordWrap(true);
layout->addWidget(info);
for (const Panel& panel : panels) {
QCheckBox* box = new QCheckBox(QString::fromStdString(panel.name));
// If the profile already has a desired value for the checkbox, use it. Otherwise
// use the default values
auto it = _uiPanels->find(panel.id);
if (it != _uiPanels->end()) {
box->setChecked(it->second);
}
else {
box->setChecked(panel.isVisible);
}
layout->addWidget(box);
_checkboxToId[box] = panel.id;
}
layout->addWidget(new Line);
{
QDialogButtonBox* buttons = new QDialogButtonBox;
buttons->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Cancel);
connect(
buttons, &QDialogButtonBox::accepted,
this, &UiPanelsDialog::parseSelections
);
connect(buttons, &QDialogButtonBox::rejected, this, &UiPanelsDialog::reject);
layout->addWidget(buttons);
}
}
void UiPanelsDialog::parseSelections() {
_uiPanels->clear();
for (const auto& [key, value] : _checkboxToId) {
_uiPanels->emplace(value, key->isChecked());
}
accept();
}

View File

@@ -0,0 +1,17 @@
{
"0": { "id": "scene", "name": "Scene", "visible": true, "enabled": true },
"1": { "id": "settings", "name": "Settings", "visible": false, "enabled": true },
"2": { "id": "navigation", "name": "Navigation", "visible": true, "enabled": true },
"3": { "id": "timePanel", "name": "Date & Time", "visible": true, "enabled": true },
"4": { "id": "sessionRecording", "name": "Session Recording", "visible": true, "enabled": true },
"5": { "id": "geoLocation", "name": "Geo Location", "visible": true, "enabled": true },
"6": { "id": "screenSpaceRenderables", "name": "ScreenSpace Renderables", "visible": true, "enabled": true },
"7": { "id": "exoplanets", "name": "Exoplanets", "visible": true, "enabled": true },
"8": { "id": "userPanels", "name": "User Panels", "visible": true, "enabled": true },
"9": { "id": "actions", "name": "Actions", "visible": true, "enabled": true },
"10": { "id": "skyBrowser", "name": "SkyBrowser", "visible": true, "enabled": true },
"11": { "id": "mission", "name": "Mission", "visible": false, "enabled": false },
"12": { "id": "flightControl", "name": "Flight Control", "visible": false, "enabled": true },
"13": { "id": "keybindingsLayout", "name": "Keybindings", "visible": true, "enabled": true },
"14": { "id": "gettingStartedTour", "name": "Getting Started Tour", "visible": true, "enabled": true }
}

View File

@@ -178,7 +178,7 @@ public:
/// Removes an asset unless the `ignoreUpdates` member is set to `true`
void removeAsset(const std::string& path);
static constexpr Version CurrentVersion = Version{ 1, 3 };
static constexpr Version CurrentVersion = Version{ 1, 4 };
Version version = CurrentVersion;
std::vector<Module> modules;
@@ -192,6 +192,7 @@ public:
std::optional<CameraType> camera;
std::vector<std::string> markNodes;
std::vector<std::string> additionalScripts;
std::map<std::string, bool> uiPanelVisibility;
bool ignoreUpdates = false;

View File

@@ -43,6 +43,7 @@ set(HEADER_FILES
include/topics/getpropertytopic.h
include/topics/luascripttopic.h
include/topics/missiontopic.h
include/topics/profiletopic.h
include/topics/sessionrecordingtopic.h
include/topics/setpropertytopic.h
include/topics/shortcuttopic.h
@@ -73,6 +74,7 @@ set(SOURCE_FILES
src/topics/getpropertytopic.cpp
src/topics/luascripttopic.cpp
src/topics/missiontopic.cpp
src/topics/profiletopic.cpp
src/topics/sessionrecordingtopic.cpp
src/topics/setpropertytopic.cpp
src/topics/shortcuttopic.cpp

View File

@@ -0,0 +1,45 @@
/*****************************************************************************************
* *
* 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_MODULE_SERVER___PROFILETOPIC___H__
#define __OPENSPACE_MODULE_SERVER___PROFILETOPIC___H__
#include <modules/server/include/topics/topic.h>
using nlohmann::json;
namespace openspace {
class ProfileTopic : public Topic {
public:
ProfileTopic() = default;
~ProfileTopic() override = default;
void handleJson(const nlohmann::json& json) override;
bool isDone() const override;
};
} // namespace openspace
#endif // __OPENSPACE_MODULE_SERVER___PROFILETOPIC___H__

View File

@@ -53,6 +53,7 @@
#include <ghoul/io/socket/tcpsocketserver.h>
#include <ghoul/io/socket/websocketserver.h>
#include <ghoul/misc/profiling.h>
#include <include/topics/profiletopic.h>
namespace {
constexpr std::string_view _loggerCat = "ServerModule: Connection";
@@ -97,6 +98,7 @@ Connection::Connection(std::unique_ptr<ghoul::io::Socket> s, std::string address
_topicFactory.registerClass<GetPropertyTopic>("get");
_topicFactory.registerClass<LuaScriptTopic>("luascript");
_topicFactory.registerClass<MissionTopic>("missions");
_topicFactory.registerClass<ProfileTopic>("profile");
_topicFactory.registerClass<SessionRecordingTopic>("sessionRecording");
_topicFactory.registerClass<SetPropertyTopic>("set");
_topicFactory.registerClass<ShortcutTopic>("shortcuts");

View File

@@ -0,0 +1,46 @@
/*****************************************************************************************
* *
* 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 <modules/server/include/topics/profiletopic.h>
#include <modules/server/include/connection.h>
#include <modules/server/include/jsonconverters.h>
#include <openspace/engine/globals.h>
#include <openspace/scene/profile.h>
namespace openspace {
bool ProfileTopic::isDone() const {
return true;
}
void ProfileTopic::handleJson(const nlohmann::json&) {
const nlohmann::json data = {
{ "uiPanelVisibility", global::profile->uiPanelVisibility },
{ "markNodes", global::profile->markNodes }
};
_connection->sendJson(wrappedPayload(data));
}
} // namespace openspace

View File

@@ -623,6 +623,15 @@ void convertVersion12to13(nlohmann::json& profile) {
} // namespace version12
namespace version13 {
void convertVersion13to14(nlohmann::json& profile) {
// Version 1.4 introduced the ui panel view
profile["version"] = Profile::Version{ 1, 4 };
}
} // namespace version13
Profile::ParsingError::ParsingError(Severity severity_, std::string msg)
: ghoul::RuntimeError(std::move(msg), "profile")
, severity(severity_)
@@ -737,6 +746,9 @@ std::string Profile::serialize() const {
if (!additionalScripts.empty()) {
r["additional_scripts"] = additionalScripts;
}
if (!uiPanelVisibility.empty()) {
r["panel_visibility"] = uiPanelVisibility;
}
return r.dump(2);
}
@@ -779,6 +791,11 @@ Profile::Profile(const std::filesystem::path& path) {
profile["version"].get_to(version);
}
if (version.major == 1 && version.minor == 3) {
version13::convertVersion13to14(profile);
profile["version"].get_to(version);
}
if (profile.find("modules") != profile.end()) {
profile["modules"].get_to(modules);
@@ -827,6 +844,9 @@ Profile::Profile(const std::filesystem::path& path) {
if (profile.find("additional_scripts") != profile.end()) {
profile["additional_scripts"].get_to(additionalScripts);
}
if (profile.find("panel_visibility") != profile.end()) {
profile["panel_visibility"].get_to(uiPanelVisibility);
}
}
catch (const nlohmann::json::exception& e) {
std::string err = e.what();

View File

@@ -0,0 +1,131 @@
{
"version": { "major": 1, "minor": 4 },
"meta": {
"name": "name",
"version": "version",
"description": "description",
"author": "author",
"url": "url",
"license": "license"
},
"modules": [
{ "name": "abs-module" },
{
"name": "def-module",
"loadedInstruction": "instr"
},
{
"name": "ghi-module",
"notLoadedInstruction": "not_instr"
},
{
"name": "jkl-module",
"loadedInstruction": "instr",
"notLoadedInstruction": "not_instr"
}
],
"assets": [
"scene/solarsystem/planets/earth/earth",
"scene/solarsystem/planets/earth/satellites/satellites",
"folder1/folder2/asset",
"folder3/folder4/asset2",
"folder5/folder6/asset3"
],
"properties": [
{
"type": "setPropertyValue",
"name": "{earth_satellites}.Renderable.Enabled",
"value": "false"
},
{
"type": "setPropertyValue",
"name": "property_name_1",
"value": "property_value_1"
},
{
"type": "setPropertyValue",
"name": "property_name_2",
"value": "property_value_2"
},
{
"type": "setPropertyValue",
"name": "property_name_3",
"value": "property_value_3"
},
{
"type": "setPropertyValueSingle",
"name": "property_name_4",
"value": "property_value_4"
},
{
"type": "setPropertyValueSingle",
"name": "property_name_5",
"value": "property_value_5"
},
{
"type": "setPropertyValueSingle",
"name": "property_name_6",
"value": "property_value_6"
}
],
"actions": [
{
"identifier": "profile.keybind.0",
"documentation": "T documentation",
"name": "T name",
"gui_path": "T Gui-Path",
"is_local": true,
"script": "T script"
},
{
"identifier": "profile.keybind.1",
"documentation": "U documentation",
"name": "U name",
"gui_path": "U Gui-Path",
"is_local": false,
"script": "U script"
},
{
"identifier": "profile.keybind.2",
"documentation": "CTRL+V documentation",
"name": "CTRL+V name",
"gui_path": "CTRL+V Gui-Path",
"is_local": false,
"script": "CTRL+V script"
}
],
"keybindings": [
{
"action": "profile.keybind.0",
"key": "T"
},
{
"action": "profile.keybind.1",
"key": "U"
},
{
"action": "profile.keybind.2",
"key": "CTRL+V"
}
],
"time": {
"type": "relative",
"value": "-1d",
"is_paused": false
},
"camera": {
"type": "goToGeo",
"anchor": "Earth",
"latitude": 58.5877,
"longitude": 16.1924,
"altitude": 2.0e+07
},
"mark_nodes": [
"Earth", "Mars", "Moon", "Sun"
],
"additional_scripts": [
"script-1",
"script-2",
"script-3"
]
}

View File

@@ -888,6 +888,15 @@ TEST_CASE("Version 1.0 -> 1.3", "[profile]") {
CHECK(src == dst);
}
TEST_CASE("Version 1.0 -> 1.4", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_10.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_14.profile";
Profile src = Profile(absPath(Src));
Profile dst = Profile(absPath(Dest));
CHECK(src == dst);
}
TEST_CASE("Version 1.1 -> 1.2", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_11.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_12.profile";
@@ -906,6 +915,15 @@ TEST_CASE("Version 1.1 -> 1.3", "[profile]") {
CHECK(src == dst);
}
TEST_CASE("Version 1.1 -> 1.4", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_11.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_14.profile";
Profile src = Profile(absPath(Src));
Profile dst = Profile(absPath(Dest));
CHECK(src == dst);
}
TEST_CASE("Version 1.2 -> 1.3", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_12.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_13.profile";
@@ -915,6 +933,24 @@ TEST_CASE("Version 1.2 -> 1.3", "[profile]") {
CHECK(src == dst);
}
TEST_CASE("Version 1.2 -> 1.4", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_12.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_14.profile";
Profile src = Profile(absPath(Src));
Profile dst = Profile(absPath(Dest));
CHECK(src == dst);
}
TEST_CASE("Version 1.3 -> 1.4", "[profile]") {
constexpr std::string_view Src = "${TESTDIR}/profile/conversion/version_13.profile";
constexpr std::string_view Dest = "${TESTDIR}/profile/conversion/version_14.profile";
Profile src = Profile(absPath(Src));
Profile dst = Profile(absPath(Dest));
CHECK(src == dst);
}
//