Allow Window Config GUI to Edit Files (#2574)

* Added pboettch/json-schema-validator submodule

* Added initial code for selecting window config to edit

* Use updated sgct submodule with json-validate

* Bump sgct submodule version reference

* Bump sgct submodule version reference

* Version checking of sgct window config in progress

* Json schema validator submodule is now in sgct

* Added support for read-only window configs, and additional changes

* More changes with opening config in window edit, plus schema files added

* Update sgct schema version with more defs to work with sgctedit schema

* Fixes to get sgct edit schema working with dialog for error messages

* Improvements in exception handling

* Improved handling of multiple sgct & json exception types

* Minor improvements in exception messages

* Extra spaces in error message output

* Fixing importing of json config data from launcher to sgctedit

* Fixed window size & position update

* Changes for preserving settings in edit vs new mode

* Changes to import settings from config file

* More changes for importing testing of config file based on file tests

* Fixed window placement dims in monitor, and some code refactoring

* Move json validation before initial read

* Add CMake copy of sgct schema file to OpenSpace post-build

* Bump sgct submodule reference

* Modify calls for json schema validation and bump to latest sgct

* Bump to new sgct repo with unit testing and updated schema

* Added first test for window config editor schema

* Finished tests for sgcteditor validation

* Code cleanup pass

* Fix of sgctedit test for remove description

* CMake and include config changes to fix build after merge

* Bump sgct submodule version

* Improve paths in sgctedit test

* Bump sgct reference for test path fix

* Check for imported monitor number being in valid range

* Make sgct config 'monitor' key/value optional

* Have 'save' or 'save as' buttons depending on edit or new modes

* Code review feedback changes

* Fix to include the last file in the user config dir list

* Addressing some PR request

* Change to pass-by-reference in editRefusalDialog

* Separating errors into summary and detailed messages in error dialog

* Disable edit button with hover text if config read shows invalid format

---------

Co-authored-by: Alexander Bock <alexander.bock@liu.se>
This commit is contained in:
Gene Payne
2023-04-14 18:28:31 -06:00
committed by GitHub
parent a421d0fbe6
commit cec6e05ff1
19 changed files with 1827 additions and 137 deletions

View File

@@ -127,6 +127,13 @@ if (UNIX AND (NOT APPLE))
target_link_libraries(OpenSpace PRIVATE Xcursor Xinerama X11)
endif ()
add_custom_command(TARGET OpenSpace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/ext/sgct/sgct.schema.json
${CMAKE_CURRENT_SOURCE_DIR}/../../config/schema/sgct.schema.json
COMMAND_EXPAND_LISTS
)
end_header("Dependency: SGCT")
begin_header("Dependency: Profile Editor")

View File

@@ -29,6 +29,8 @@
#include "sgctedit/sgctedit.h"
#include <openspace/scene/profile.h>
#include <sgct/error.h>
#include <sgct/readconfig.h>
#include <QApplication>
#include <optional>
@@ -79,15 +81,28 @@ public:
*/
std::string selectedWindowConfig() const;
/**
* Returns true if the window configuration filename selected in the combo box
* is a file in the user configurations section
*
* \return true if window configuration is a user configuration file
*/
bool isUserConfigSelected() const;
private:
QWidget* createCentralWidget();
void setBackgroundImage(const std::string& syncPath);
void openProfileEditor(const std::string& profile, bool isUserProfile);
void openWindowEditor();
void openWindowEditor(const std::string& winCfg, bool isUserWinCfg);
void editRefusalDialog(const std::string& title, const std::string& msg,
const std::string& detailedText);
void populateProfilesList(std::string preset);
void populateWindowConfigsList(std::string preset);
void handleReturnFromWindowEditor(const sgct::config::Cluster& cluster,
std::filesystem::path savePath, const std::string& saveWindowCfgPath);
bool versionCheck(sgct::config::GeneratorVersion& v) const;
const std::string _assetPath;
const std::string _userAssetPath;
@@ -95,14 +110,19 @@ private:
const std::string _userConfigPath;
const std::string _profilePath;
const std::string _userProfilePath;
const std::vector<std::string>& _readOnlyWindowConfigs;
const std::vector<std::string>& _readOnlyProfiles;
bool _shouldLaunch = false;
int _userAssetCount = 0;
int _userConfigStartingIdx = 0;
int _userConfigCount = 0;
int _preDefinedConfigStartingIdx = 0;
const std::string _sgctConfigName;
int _windowConfigBoxIndexSgctCfgDefault = 0;
QComboBox* _profileBox = nullptr;
QComboBox* _windowConfigBox = nullptr;
QLabel* _backgroundImage = nullptr;
QPushButton* _editWindowButton = nullptr;
};
#endif // __OPENSPACE_UI_LAUNCHER___LAUNCHERWINDOW___H__

View File

@@ -48,10 +48,12 @@ public:
* \param winColors An array of QColor objects for window colors. The indexing of
* this array matches the window indexing used elsewhere in the
* class. This allows for a unique color for each window.
* \param resetToDefault If set to true, all display and window settings will be
* initialized to their default values.
* \param parent The parent to which this widget belongs
*/
DisplayWindowUnion(const std::vector<QRect>& monitorSizeList,
int nMaxWindows, const std::array<QColor, 4>& windowColors,
int nMaxWindows, const std::array<QColor, 4>& windowColors, bool resetToDefault,
QWidget* parent = nullptr);
/**
@@ -59,7 +61,15 @@ public:
*
* \return vector of pointers of WindowControl objects
*/
std::vector<WindowControl*> windowControls() const;
std::vector<WindowControl*> activeWindowControls() const;
/**
* Returns a vector of pointers to the WindowControl objects for all windows, whether
* they are visible or not.
*
* \return vector of pointers of all WindowControl objects
*/
std::vector<WindowControl*>& windowControls();
/**
* When called will add a new window to the set of windows, which will, in turn, send
@@ -73,6 +83,14 @@ public:
*/
void removeWindow();
/**
* Returns the number of windows that are displayed (there can be more window
* objects than are currently displayed).
*
* \return the number of displayed windows in the current configuration
*/
unsigned int numWindowsDisplayed() const;
signals:
/**
* This signal is emitted when a windowhas changed.
@@ -92,7 +110,7 @@ signals:
private:
void createWidgets(int nMaxWindows, std::vector<QRect> monitorResolutions,
std::array<QColor, 4> windowColors);
std::array<QColor, 4> windowColors, bool resetToDefault);
void showWindows();
unsigned int _nWindowsDisplayed = 0;

View File

@@ -62,6 +62,23 @@ public:
*/
bool showUiOnFirstWindow() const;
/**
* Sets the value of the checkbox for putting the GUI only on the first window.
* If this is enabled, then the first window will draw2D but not draw3D. All
* subsequent windows will be the opposite of this.
*
* \param setUiOnFirstWindow boolean value, if set true then the GUI will only
* be on the first window
*/
void setShowUiOnFirstWindow(bool setUiOnFirstWindow);
/**
* Sets the value of the checkbox for enabling VSync.
*
* \param enableVsync boolean value, if set true then VSync is enabled
*/
void setVsync(bool enableVsync);
private:
sgct::quat _orientationValue = sgct::quat(0.f, 0.f, 0.f, 0.f);
QCheckBox* _checkBoxVsync = nullptr;

View File

@@ -28,6 +28,7 @@
#include <QDialog>
#include <sgct/config.h>
#include <sgctedit/windowcontrol.h>
#include <QColor>
#include <array>
#include <filesystem>
@@ -38,12 +39,16 @@ class SettingsWidget;
class QBoxLayout;
class QWidget;
const sgct::config::GeneratorVersion versionMin { "SgctWindowConfig", 1, 1 };
const sgct::config::GeneratorVersion versionLegacy18 { "OpenSpace", 0, 18 };
const sgct::config::GeneratorVersion versionLegacy19 { "OpenSpace", 0, 19 };
class SgctEdit final : public QDialog {
Q_OBJECT
public:
/**
* Constructor for SgctEdit class, the underlying class for the full window
* configuration editor
* configuration editor. Used when creating a new config.
*
* \param parent The Qt QWidget parent object
* \param userConfigPath A string containing the file path of the user config
@@ -51,6 +56,22 @@ public:
*/
SgctEdit(QWidget* parent, std::string userConfigPath);
/**
* Constructor for SgctEdit class, the underlying class for the full window
* configuration editor. Used when editing an existing config.
*
* \param cluster The #sgct::config::Cluster object containing all data of the
* imported window cluster configuration.
* \param configName The name of the window configuration filename
* \param configBasePath The path to the folder where default config files reside
* \param configsReadOnly vector list of window config names that are read-only and
* must not be overwritten
* \param parent Pointer to parent Qt widget
*/
SgctEdit(sgct::config::Cluster& cluster, const std::string& configName,
std::string& configBasePath, const std::vector<std::string>& configsReadOnly,
QWidget* parent);
/**
* Returns the saved filename
*
@@ -66,8 +87,16 @@ public:
sgct::config::Cluster cluster() const;
private:
void createWidgets(const std::vector<QRect>& monitorSizes);
sgct::config::Cluster generateConfiguration() const;
std::vector<QRect> createMonitorInfoSet();
void createWidgets(const std::vector<QRect>& monitorSizes, unsigned int nWindows,
bool setToDefaults);
void generateConfiguration();
void generateConfigSetupVsync();
void generateConfigUsers();
void generateConfigAddresses(sgct::config::Node& node);
void generateConfigResizeWindowsAccordingToSelected(sgct::config::Node& node);
void generateConfigIndividualWindowSettings(sgct::config::Node& node);
void setupProjectionTypeInGui(sgct::config::Viewport& vPort, WindowControl* wCtrl);
void save();
void apply();
@@ -82,12 +111,15 @@ private:
QColor(0x44, 0xAF, 0x69),
QColor(0xF8, 0x33, 0x3C)
};
std::string _configurationFilename;
const std::vector<std::string> _readOnlyConfigs;
QBoxLayout* _layoutButtonBox = nullptr;
QPushButton* _saveButton = nullptr;
QPushButton* _cancelButton = nullptr;
QPushButton* _applyButton = nullptr;
std::string _saveTarget;
bool _didImportValues = false;
};
#endif // __OPENSPACE_UI_LAUNCHER___SGCTEDIT___H__

View File

@@ -41,6 +41,14 @@ class QSpinBox;
class WindowControl final : public QWidget {
Q_OBJECT
public:
enum class ProjectionIndices {
Planar = 0,
Fisheye,
SphericalMirror,
Cylindrical,
Equirectangular
};
/**
* Constructor for WindowControl class, which contains settings and configuration
* for individual windows
@@ -52,7 +60,8 @@ public:
* \param winColor A QColor object for this window's unique color
*/
WindowControl(int monitorIndex, int windowIndex,
const std::vector<QRect>& monitorDims, const QColor& winColor, QWidget* parent);
const std::vector<QRect>& monitorDims, const QColor& winColor,
bool resetToDefault, QWidget* parent);
/**
* Makes the window label at top of a window control column visible
@@ -66,20 +75,100 @@ public:
*/
void resetToDefaults();
sgct::config::Window generateWindowInformation() const;
/**
* Sets the window dimensions
*
* \param newDims The x, y dimensions to set the window to
*/
void setDimensions(QRectF newDims);
/**
* Sets the monitor selection combobox
*
* \param monitorIndex The zero-based monitor index to set the combobox selection to
*/
void setMonitorSelection(int monitorIndex);
/**
* Sets the window name in the text edit box
*
* \param windowName The window title to set
*/
void setWindowName(const std::string& windowName);
/**
* Sets the window's decoration status. If set to true, then the window has a
* border. If false it is borderless
*
* \param hasWindowDecoration boolean for if window has decoration (border)
*/
void setDecorationState(bool hasWindowDecoration);
/**
* Generates window configuration (sgct::config::Window struct) based on the
* GUI settings.
*
* \param window The sgct::config::Window struct that is passed into the function
* and modified with the generated window content
*/
void generateWindowInformation(sgct::config::Window& window) const;
/**
* Sets the window's projection type to planar, with the accompanying parameters
* for horizontal and vertical FOV.
*
* \param hfov float value for horizontal field of view angle (degrees)
* \param vfov float value for vertical field of view angle (degrees)
*/
void setProjectionPlanar(float hfov, float vfov);
/**
* Sets the window's projection type to fisheye, with the accompanying quality
* setting and spout option
*
* \param quality int value for number of vertical lines of resolution. This will
* be compared against the QualityValues array in order to set the
* correct combobox index
* \param spoutOutput bool for enabling the spout output option
*/
void setProjectionFisheye(int quality, bool spoutOutput);
/**
* Sets the window's projection type to spherical mirror, with the accompanying
* quality setting
*
* \param quality int value for number of vertical lines of resolution. This will
* be compared against the QualityValues array in order to set the
* correct combobox index
*/
void setProjectionSphericalMirror(int quality);
/**
* Sets the window's projection type to cylindrical, with the accompanying quality
* setting and height offset value
*
* \param quality int value for number of vertical lines of resolution. This will
* be compared against the QualityValues array in order to set the
* correct combobox index
* \param heightOffset float value for height offset to be applied
*/
void setProjectionCylindrical(int quality, float heightOffset);
/**
* Sets the window's projection type to equirectangular, with the accompanying
* quality setting and spout option
*
* \param quality int value for number of vertical lines of resolution. This will
* be compared against the QualityValues array in order to set the
* correct combobox index
* \param spoutOutput bool for enabling the spout output option
*/
void setProjectionEquirectangular(int quality, bool spoutOutput);
signals:
void windowChanged(int monitorIndex, int windowIndex, const QRectF& newDimensions);
private:
enum class ProjectionIndices {
Planar = 0,
Fisheye,
SphericalMirror,
Cylindrical,
Equirectangular
};
void createWidgets(const QColor& windowColor);
QWidget* createPlanarWidget();
QWidget* createFisheyeWidget();
@@ -98,6 +187,7 @@ private:
sgct::config::Projections generateProjectionInformation() const;
void updatePlanarLockedFov();
void setQualityComboBoxFromLinesResolution(int lines, QComboBox* combo);
static constexpr float IdealAspectRatio = 16.f / 9.f;
float _aspectRatioSize = IdealAspectRatio;

View File

@@ -175,16 +175,12 @@ namespace {
}
void saveWindowConfig(QWidget* parent, const std::filesystem::path& path,
sgct::config::Cluster& cluster)
const sgct::config::Cluster& cluster)
{
std::ofstream outFile;
try {
outFile.open(path, std::ofstream::out);
sgct::config::GeneratorVersion genEntry = sgct::config::GeneratorVersion{
"OpenSpace",
OPENSPACE_VERSION_MAJOR,
OPENSPACE_VERSION_MINOR
};
sgct::config::GeneratorVersion genEntry = versionMin;
outFile << sgct::serializeConfig(
cluster,
genEntry
@@ -217,6 +213,7 @@ LauncherWindow::LauncherWindow(bool profileEnabled,
, _userProfilePath(
absPath(globalConfig.pathTokens.at("USER_PROFILES")).string() + '/'
)
, _readOnlyWindowConfigs(globalConfig.readOnlyWindowConfigs)
, _readOnlyProfiles(globalConfig.readOnlyProfiles)
, _sgctConfigName(sgctConfigName)
{
@@ -246,9 +243,10 @@ LauncherWindow::LauncherWindow(bool profileEnabled,
populateProfilesList(globalConfig.profile);
_profileBox->setEnabled(profileEnabled);
populateWindowConfigsList(_sgctConfigName);
_windowConfigBox->setEnabled(sgctConfigEnabled);
populateWindowConfigsList(_sgctConfigName);
// Trigger currentIndexChanged so the preview file read is performed
_windowConfigBox->currentIndexChanged(_windowConfigBox->currentIndex());
std::filesystem::path p = absPath(
globalConfig.pathTokens.at("SYNC") + "/http/launcher_images"
@@ -343,13 +341,31 @@ QWidget* LauncherWindow::createCentralWidget() {
connect(
newWindowButton, &QPushButton::released,
[this]() {
openWindowEditor();
openWindowEditor("", true);
}
);
newWindowButton->setObjectName("small");
newWindowButton->setGeometry(geometry::NewWindowButton);
newWindowButton->setCursor(Qt::PointingHandCursor);
_editWindowButton = new QPushButton("Edit", centralWidget);
connect(
_editWindowButton,
&QPushButton::released,
[this]() {
std::filesystem::path pathSelected = absPath(selectedWindowConfig());
bool isUserConfig = isUserConfigSelected();
std::string fileSelected = pathSelected.generic_string();
if (std::filesystem::is_regular_file(pathSelected)) {
openWindowEditor(fileSelected, isUserConfig);
}
}
);
_editWindowButton->setVisible(true);
_editWindowButton->setObjectName("small");
_editWindowButton->setGeometry(geometry::EditWindowButton);
_editWindowButton->setCursor(Qt::PointingHandCursor);
return centralWidget;
}
@@ -537,15 +553,26 @@ bool handleConfigurationFile(QComboBox& box, const std::filesystem::directory_en
void LauncherWindow::populateWindowConfigsList(std::string preset) {
namespace fs = std::filesystem;
// Disconnect the signal for new window config selection during population process
disconnect(
_windowConfigBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
nullptr,
nullptr
);
_windowConfigBox->clear();
_userConfigCount = 0;
_userConfigStartingIdx = 0;
_preDefinedConfigStartingIdx = 0;
_windowConfigBox->addItem(QString::fromStdString("--- User Configurations ---"));
const QStandardItemModel* model =
qobject_cast<const QStandardItemModel*>(_windowConfigBox->model());
model->item(_userConfigCount)->setEnabled(false);
++_userConfigCount;
_userConfigCount++;
_userConfigStartingIdx++;
_preDefinedConfigStartingIdx++;
bool hasXmlConfig = false;
@@ -560,14 +587,16 @@ void LauncherWindow::populateWindowConfigsList(std::string preset) {
for (const fs::directory_entry& p : files) {
bool isConfigFile = handleConfigurationFile(*_windowConfigBox, p);
if (isConfigFile) {
++_userConfigCount;
_userConfigCount++;
_userConfigStartingIdx++;
_preDefinedConfigStartingIdx++;
}
hasXmlConfig |= p.path().extension() == ".xml";
}
_windowConfigBox->addItem(QString::fromStdString("--- OpenSpace Configurations ---"));
model = qobject_cast<const QStandardItemModel*>(_windowConfigBox->model());
model->item(_userConfigCount)->setEnabled(false);
_preDefinedConfigStartingIdx++;
if (std::filesystem::exists(_configPath)) {
// Sort files
@@ -601,7 +630,10 @@ void LauncherWindow::populateWindowConfigsList(std::string preset) {
}
// Always add the .cfg sgct default as first item
_windowConfigBox->insertItem(0, QString::fromStdString(_sgctConfigName));
_windowConfigBox->insertItem(
_windowConfigBoxIndexSgctCfgDefault,
QString::fromStdString(_sgctConfigName)
);
// Try to find the requested configuration file and set it as the current one. As we
// have support for function-generated configuration files that will not be in the
// list we need to add a preset that doesn't exist a file for
@@ -611,12 +643,63 @@ void LauncherWindow::populateWindowConfigsList(std::string preset) {
}
else {
// Add the requested preset at the top
_windowConfigBox->insertItem(1, QString::fromStdString(preset));
_windowConfigBox->insertItem(
_windowConfigBoxIndexSgctCfgDefault + 1,
QString::fromStdString(preset)
);
// Increment the user config count because there is an additional option added
// before the user config options
_userConfigCount++;
_windowConfigBox->setCurrentIndex(1);
_userConfigStartingIdx++;
_preDefinedConfigStartingIdx++;
_windowConfigBox->setCurrentIndex(_windowConfigBoxIndexSgctCfgDefault + 1);
}
connect(
_windowConfigBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int newIndex) {
std::filesystem::path pathSelected = absPath(selectedWindowConfig());
std::string fileSelected = pathSelected.generic_string();
if (newIndex == _windowConfigBoxIndexSgctCfgDefault) {
_editWindowButton->setEnabled(false);
_editWindowButton->setToolTip(
"Cannot edit the 'Default' configuration since it is not a file"
);
}
else if (newIndex >= _preDefinedConfigStartingIdx) {
_editWindowButton->setEnabled(false);
_editWindowButton->setToolTip(
QString::fromStdString(fmt::format(
"Cannot edit '{}' since it is one of the configuration "
"files provided in the OpenSpace installation", fileSelected))
);
}
else {
try {
sgct::config::GeneratorVersion previewGenVersion =
sgct::readConfigGenerator(fileSelected);
if (!versionCheck(previewGenVersion)) {
_editWindowButton->setEnabled(false);
_editWindowButton->setToolTip(QString::fromStdString(fmt::format(
"This file does not meet the minimum required version of {}.",
versionMin.versionString()
)));
return;
}
}
catch (const std::runtime_error& e) {
// Ignore an exception here because clicking the edit button will
// bring up an explanatory error message
}
_editWindowButton->setEnabled(true);
_editWindowButton->setToolTip("");
}
}
);
}
bool LauncherWindow::versionCheck(sgct::config::GeneratorVersion& v) const {
return (v.versionCheck(versionMin) || v == versionLegacy18 || v == versionLegacy19);
}
void LauncherWindow::openProfileEditor(const std::string& profile, bool isUserProfile) {
@@ -659,18 +742,112 @@ void LauncherWindow::openProfileEditor(const std::string& profile, bool isUserPr
}
}
void LauncherWindow::openWindowEditor() {
SgctEdit editor(this, _userConfigPath);
int ret = editor.exec();
if (ret == QDialog::DialogCode::Accepted) {
sgct::config::Cluster cluster = editor.cluster();
void LauncherWindow::editRefusalDialog(const std::string& title, const std::string& msg,
const std::string& detailedText)
{
QMessageBox msgBox(this);
msgBox.setText(QString::fromStdString(msg));
msgBox.setWindowTitle(QString::fromStdString(title));
msgBox.setDetailedText(QString::fromStdString(detailedText));
msgBox.setIcon(QMessageBox::Warning);
msgBox.exec();
}
std::filesystem::path savePath = editor.saveFilename();
saveWindowConfig(this, savePath, cluster);
// Truncate path to convert this back to path relative to _userConfigPath
std::string p = savePath.string().substr(_userConfigPath.size());
populateWindowConfigsList(p);
void LauncherWindow::openWindowEditor(const std::string& winCfg, bool isUserWinCfg) {
using namespace sgct;
std::string saveWindowCfgPath = isUserWinCfg ? _userConfigPath : _configPath;
int ret = QDialog::DialogCode::Rejected;
config::Cluster preview;
if (winCfg.empty()) {
SgctEdit editor(this, _userConfigPath);
ret = editor.exec();
if (ret == QDialog::DialogCode::Accepted) {
handleReturnFromWindowEditor(
editor.cluster(),
editor.saveFilename(),
saveWindowCfgPath
);
}
}
else {
try {
config::GeneratorVersion previewGenVersion = readConfigGenerator(winCfg);
loadFileAndSchemaThenValidate(
winCfg,
_configPath + "/schema/sgct.schema.json",
"This configuration file is unable to generate a proper display"
);
loadFileAndSchemaThenValidate(
winCfg,
_configPath + "/schema/sgcteditor.schema.json",
"This configuration file is valid for generating a display, but "
"its format does not match the window editor requirements and "
"cannot be opened in the editor"
);
if (versionCheck(previewGenVersion)) {
try {
preview = readConfig(
winCfg,
"This configuration file is unable to generate a proper display "
"due to a problem detected in the readConfig function"
);
}
catch (const std::runtime_error& e) {
//Re-throw an SGCT error exception with the runtime exception message
throw std::runtime_error(
fmt::format(
"Importing of this configuration file failed because of a "
"problem detected in the readConfig function:\n\n{}", e.what()
)
);
}
SgctEdit editor(
preview,
winCfg,
saveWindowCfgPath,
_readOnlyWindowConfigs,
this
);
ret = editor.exec();
if (ret == QDialog::DialogCode::Accepted) {
handleReturnFromWindowEditor(
editor.cluster(),
editor.saveFilename(),
saveWindowCfgPath
);
}
}
else {
editRefusalDialog(
"File Format Version Error",
fmt::format(
"File '{}' does not meet the minimum required version of {}.",
winCfg, versionMin.versionString()
),
""
);
}
}
catch (const std::runtime_error& e) {
editRefusalDialog(
"Format Validation Error",
fmt::format("Parsing error found in file '{}'", winCfg),
e.what()
);
}
}
}
void LauncherWindow::handleReturnFromWindowEditor(const sgct::config::Cluster& cluster,
std::filesystem::path savePath,
const std::string& saveWindowCfgPath)
{
savePath.replace_extension(".json");
saveWindowConfig(this, savePath, cluster);
// Truncate path to convert this back to path relative to _userConfigPath
std::string p = std::filesystem::proximate(savePath, saveWindowCfgPath).string();
populateWindowConfigsList(p);
}
bool LauncherWindow::wasLaunchSelected() const {
@@ -694,3 +871,8 @@ std::string LauncherWindow::selectedWindowConfig() const {
return "${USER_CONFIG}/" + _windowConfigBox->currentText().toStdString();
}
}
bool LauncherWindow::isUserConfigSelected() const {
int selectedIndex = _windowConfigBox->currentIndex();
return (selectedIndex <= _userConfigCount);
}

View File

@@ -36,16 +36,22 @@
DisplayWindowUnion::DisplayWindowUnion(const std::vector<QRect>& monitorSizeList,
int nMaxWindows,
const std::array<QColor, 4>& windowColors,
QWidget* parent)
bool resetToDefault, QWidget* parent)
: QWidget(parent)
{
createWidgets(nMaxWindows, monitorSizeList, windowColors);
createWidgets(
nMaxWindows,
monitorSizeList,
windowColors,
resetToDefault
);
showWindows();
}
void DisplayWindowUnion::createWidgets(int nMaxWindows,
std::vector<QRect> monitorResolutions,
std::array<QColor, 4> windowColors)
std::array<QColor, 4> windowColors,
bool resetToDefault)
{
// Add all window controls (some will be hidden from GUI initially)
for (int i = 0; i < nMaxWindows; ++i) {
@@ -57,6 +63,7 @@ void DisplayWindowUnion::createWidgets(int nMaxWindows,
i,
monitorResolutions,
windowColors[i],
resetToDefault,
this
);
_windowControl.push_back(ctrl);
@@ -120,7 +127,7 @@ void DisplayWindowUnion::createWidgets(int nMaxWindows,
layout->addStretch();
}
std::vector<WindowControl*> DisplayWindowUnion::windowControls() const {
std::vector<WindowControl*> DisplayWindowUnion::activeWindowControls() const {
std::vector<WindowControl*> res;
res.reserve(_nWindowsDisplayed);
for (unsigned int i = 0; i < _nWindowsDisplayed; ++i) {
@@ -129,6 +136,10 @@ std::vector<WindowControl*> DisplayWindowUnion::windowControls() const {
return res;
}
std::vector<WindowControl*>& DisplayWindowUnion::windowControls() {
return _windowControl;
}
void DisplayWindowUnion::addWindow() {
if (_nWindowsDisplayed < _windowControl.size()) {
_windowControl[_nWindowsDisplayed]->resetToDefaults();
@@ -144,6 +155,10 @@ void DisplayWindowUnion::removeWindow() {
}
}
unsigned int DisplayWindowUnion::numWindowsDisplayed() const {
return _nWindowsDisplayed;
}
void DisplayWindowUnion::showWindows() {
for (size_t i = 0; i < _windowControl.size(); ++i) {
_windowControl[i]->setVisible(i < _nWindowsDisplayed);

View File

@@ -80,3 +80,11 @@ bool SettingsWidget::vsync() const {
bool SettingsWidget::showUiOnFirstWindow() const {
return _showUiOnFirstWindow->isChecked();
}
void SettingsWidget::setShowUiOnFirstWindow(bool setUiOnFirstWindow) {
_showUiOnFirstWindow->setChecked(setUiOnFirstWindow);
}
void SettingsWidget::setVsync(bool enableVsync) {
_checkBoxVsync->setChecked(enableVsync);
}

View File

@@ -27,7 +27,6 @@
#include <sgctedit/displaywindowunion.h>
#include <sgctedit/monitorbox.h>
#include <sgctedit/settingswidget.h>
#include <sgctedit/windowcontrol.h>
#include <ghoul/filesystem/filesystem.h>
#include <QApplication>
#include <QFileDialog>
@@ -65,15 +64,160 @@ namespace {
// We got to the end without running into any problems, so we are golden
return false;
}
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
} // namespace
SgctEdit::SgctEdit(QWidget* parent, std::string userConfigPath)
: QDialog(parent)
, _userConfigPath(std::move(userConfigPath))
{
QList<QScreen*> screens = qApp->screens();
setWindowTitle("Window Configuration Editor");
createWidgets(createMonitorInfoSet(), 1, true);
}
SgctEdit::SgctEdit(sgct::config::Cluster& cluster, const std::string& configName,
std::string& configBasePath,
const std::vector<std::string>& configsReadOnly, QWidget* parent)
: QDialog(parent)
, _cluster(cluster)
, _userConfigPath(configBasePath)
, _configurationFilename(configName)
, _readOnlyConfigs(configsReadOnly)
, _didImportValues(true)
{
setWindowTitle("Window Configuration Editor");
size_t nWindows = _cluster.nodes.front().windows.size();
bool firstWindowGuiIsEnabled = (nWindows > 1);
std::vector<QRect> monitorSizes = createMonitorInfoSet();
createWidgets(monitorSizes, nWindows, false);
size_t existingWindowsControlSize = _displayWidget->windowControls().size();
for (size_t i = 0; i < nWindows; ++i) {
sgct::config::Window& w = _cluster.nodes.front().windows[i];
WindowControl* wCtrl = _displayWidget->windowControls()[i];
if (i < existingWindowsControlSize && wCtrl) {
unsigned int monitorNum = 0;
if (w.monitor) {
monitorNum = static_cast<unsigned int>(w.monitor.value());
if (monitorNum > (monitorSizes.size() - 1)) {
monitorNum = 0;
}
}
unsigned int posX = 0;
unsigned int posY = 0;
wCtrl->setMonitorSelection(monitorNum);
if (w.pos.has_value()) {
posX = w.pos.value().x;
posY = w.pos.value().y;
// Convert offsets to coordinates relative to the selected monitor bounds,
// since window offsets are stored n the sgct config file relative to the
// coordinates of the total "canvas" of all displays
if (monitorSizes.size() > monitorNum) {
posX -= monitorSizes[monitorNum].x();
posY -= monitorSizes[monitorNum].y();
}
}
QRectF newDims(
posX,
posY,
w.size.x,
w.size.y
);
wCtrl->setDimensions(newDims);
if (w.name.has_value()) {
wCtrl->setWindowName(w.name.value());
}
wCtrl->setDecorationState(w.isDecorated.value());
}
// If first window only renders 2D, and all subsequent windows render 3D, then
// will enable the checkbox option for showing GUI only on first window
if (w.draw2D.has_value() && w.draw3D.has_value()) {
firstWindowGuiIsEnabled &= (i == 0) ? w.draw2D.value() : !w.draw2D.value();
firstWindowGuiIsEnabled &= (i == 0) ? !w.draw3D.value() : w.draw3D.value();
}
else {
firstWindowGuiIsEnabled = false;
}
setupProjectionTypeInGui(w.viewports.back(), wCtrl);
}
_settingsWidget->setShowUiOnFirstWindow(firstWindowGuiIsEnabled);
_settingsWidget->setVsync(
_cluster.settings &&
_cluster.settings.value().display &&
_cluster.settings.value().display.value().swapInterval
);
}
void SgctEdit::setupProjectionTypeInGui(sgct::config::Viewport& vPort,
WindowControl* wCtrl)
{
std::visit(overloaded{
[&](sgct::config::CylindricalProjection p) {
if (p.quality && p.heightOffset) {
wCtrl->setProjectionCylindrical(
*p.quality,
*p.heightOffset
);
}
},
[&](sgct::config::EquirectangularProjection p) {
if (p.quality) {
wCtrl->setProjectionEquirectangular(
*p.quality,
false
);
}
},
[&](sgct::config::FisheyeProjection p) {
if (p.quality) {
wCtrl->setProjectionFisheye(
*p.quality,
false
);
}
},
[&](sgct::config::PlanarProjection p) {
wCtrl->setProjectionPlanar(
(std::abs(p.fov.left) + std::abs(p.fov.right)),
(std::abs(p.fov.up) + std::abs(p.fov.down))
);
},
[&](sgct::config::SphericalMirrorProjection p) {
if (p.quality) {
wCtrl->setProjectionSphericalMirror(
*p.quality
);
}
},
[&](sgct::config::SpoutOutputProjection p) {
if (p.quality) {
if (p.mapping ==
sgct::config::SpoutOutputProjection::Mapping::Equirectangular)
{
wCtrl->setProjectionEquirectangular(
*p.quality,
true
);
}
else if (p.mapping ==
sgct::config::SpoutOutputProjection::Mapping::Fisheye)
{
wCtrl->setProjectionFisheye(
*p.quality,
true
);
}
}
},
[&](sgct::config::NoProjection) {},
[&](sgct::config::ProjectionPlane) {},
[&](sgct::config::SpoutFlatProjection) {}
}, vPort.projection);
}
std::vector<QRect> SgctEdit::createMonitorInfoSet() {
QList<QScreen*> screens = qApp->screens();
int nScreensManaged = std::min(static_cast<int>(screens.length()), 4);
std::vector<QRect> monitorSizes;
for (int s = 0; s < nScreensManaged; ++s) {
@@ -88,11 +232,12 @@ SgctEdit::SgctEdit(QWidget* parent, std::string userConfigPath)
static_cast<int>(actualHeight * screens[s]->devicePixelRatio())
);
}
createWidgets(monitorSizes);
return monitorSizes;
}
void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes) {
void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes,
unsigned int nWindows, bool setToDefaults)
{
QBoxLayout* layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize);
@@ -118,6 +263,7 @@ void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes) {
monitorSizes,
MaxNumberWindows,
_colorsForWindows,
setToDefaults,
this
);
connect(
@@ -128,7 +274,9 @@ void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes) {
_displayWidget, &DisplayWindowUnion::nWindowsChanged,
monitorBox, &MonitorBox::nWindowsDisplayedChanged
);
_displayWidget->addWindow();
for (unsigned int i = 0; i < nWindows; ++i) {
_displayWidget->addWindow();
}
displayLayout->addWidget(_displayWidget);
@@ -152,7 +300,8 @@ void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes) {
connect(_cancelButton, &QPushButton::released, this, &SgctEdit::reject);
layoutButtonBox->addWidget(_cancelButton);
_saveButton = new QPushButton("Save As");
_saveButton = _didImportValues ?
new QPushButton("Save") : new QPushButton("Save As");
_saveButton->setToolTip("Save configuration changes");
_saveButton->setFocusPolicy(Qt::NoFocus);
connect(_saveButton, &QPushButton::released, this, &SgctEdit::save);
@@ -173,8 +322,8 @@ std::filesystem::path SgctEdit::saveFilename() const {
}
void SgctEdit::save() {
sgct::config::Cluster cluster = generateConfiguration();
if (hasWindowIssues(cluster)) {
generateConfiguration();
if (hasWindowIssues(_cluster)) {
int ret = QMessageBox::warning(
this,
"Window Sizes Incompatible",
@@ -190,27 +339,32 @@ void SgctEdit::save() {
}
}
QString fileName = QFileDialog::getSaveFileName(
this,
"Save Window Configuration File",
QString::fromStdString(_userConfigPath),
"Window Configuration (*.json)",
nullptr
#ifdef __linux__
// Linux in Qt5 and Qt6 crashes when trying to access the native dialog here
, QFileDialog::DontUseNativeDialog
#endif
);
if (!fileName.isEmpty()) {
_saveTarget = fileName.toStdString();
_cluster = std::move(cluster);
if (_didImportValues) {
_saveTarget = _configurationFilename;
accept();
}
else {
QString fileName = QFileDialog::getSaveFileName(
this,
"Save Window Configuration File",
QString::fromStdString(_userConfigPath),
"Window Configuration (*.json)",
nullptr
#ifdef __linux__
// Linux in Qt5 and Qt6 crashes when trying to access the native dialog here
, QFileDialog::DontUseNativeDialog
#endif
);
if (!fileName.isEmpty()) {
_saveTarget = fileName.toStdString();
accept();
}
}
}
void SgctEdit::apply() {
sgct::config::Cluster cluster = generateConfiguration();
if (hasWindowIssues(cluster)) {
generateConfiguration();
if (hasWindowIssues(_cluster)) {
int ret = QMessageBox::warning(
this,
"Window Sizes Incompatible",
@@ -235,63 +389,101 @@ void SgctEdit::apply() {
std::filesystem::create_directories(absPath(userCfgTempDir));
}
_saveTarget = userCfgTempDir + "/apply-without-saving.json";
_cluster = std::move(cluster);
accept();
}
sgct::config::Cluster SgctEdit::generateConfiguration() const {
sgct::config::Cluster cluster;
void SgctEdit::generateConfiguration() {
_cluster.scene = sgct::config::Scene();
_cluster.scene->orientation = _settingsWidget->orientation();
if (_cluster.nodes.empty()) {
_cluster.nodes.push_back(sgct::config::Node());
}
sgct::config::Node& node = _cluster.nodes.back();
sgct::config::Scene scene;
scene.orientation = _settingsWidget->orientation();
cluster.scene = std::move(scene);
cluster.masterAddress = "localhost";
generateConfigSetupVsync();
generateConfigUsers();
generateConfigAddresses(node);
generateConfigResizeWindowsAccordingToSelected(node);
generateConfigIndividualWindowSettings(node);
}
void SgctEdit::generateConfigSetupVsync() {
if (_settingsWidget->vsync()) {
sgct::config::Settings::Display display;
display.swapInterval = 1;
sgct::config::Settings settings;
settings.display = display;
cluster.settings = settings;
}
sgct::config::Node node;
node.address = "localhost";
node.port = 20401;
// Save Windows
unsigned int windowIndex = 0;
for (WindowControl* wCtrl : _displayWidget->windowControls()) {
sgct::config::Window window = wCtrl->generateWindowInformation();
window.id = windowIndex++;
node.windows.push_back(std::move(window));
}
if (_settingsWidget->showUiOnFirstWindow()) {
sgct::config::Window& window = node.windows.front();
window.viewports.back().isTracked = false;
window.tags.push_back("GUI");
window.draw2D = true;
window.draw3D = false;
// Disable 2D rendering on all non-GUI windows
for (size_t w = 1; w < node.windows.size(); ++w) {
node.windows[w].draw2D = false;
if (!_cluster.settings || !_cluster.settings->display ||
!_cluster.settings->display->swapInterval)
{
sgct::config::Settings::Display display;
display.swapInterval = 1;
sgct::config::Settings settings;
settings.display = display;
_cluster.settings = settings;
}
}
else {
_cluster.settings = std::nullopt;
}
}
cluster.nodes.push_back(node);
void SgctEdit::generateConfigUsers() {
if (!_didImportValues) {
sgct::config::User user;
user.eyeSeparation = 0.065f;
user.position = { 0.f, 0.f, 4.f };
_cluster.users = { user };
}
}
sgct::config::User user;
user.eyeSeparation = 0.065f;
user.position = sgct::vec3(0.f, 0.f, 4.f);
cluster.users = { user };
void SgctEdit::generateConfigAddresses(sgct::config::Node& node) {
if (!_didImportValues) {
_cluster.masterAddress = "localhost";
node.address = "localhost";
node.port = 20401;
}
}
return cluster;
void SgctEdit::generateConfigResizeWindowsAccordingToSelected(sgct::config::Node& node) {
std::vector<WindowControl*> windowControls = _displayWidget->activeWindowControls();
for (size_t wIdx = 0; wIdx < windowControls.size(); ++wIdx) {
if (node.windows.size() <= wIdx) {
node.windows.push_back(sgct::config::Window());
}
if (windowControls[wIdx]) {
windowControls[wIdx]->generateWindowInformation(
node.windows[wIdx]
);
}
}
while (node.windows.size() > windowControls.size()) {
node.windows.pop_back();
}
}
void SgctEdit::generateConfigIndividualWindowSettings(sgct::config::Node& node) {
for (size_t i = 0; i < node.windows.size(); ++i) {
// First apply default settings to each window...
node.windows[i].id = i;
node.windows[i].draw2D = true;
node.windows[i].draw3D = true;
node.windows[i].viewports.back().isTracked = true;
node.windows[i].tags.erase(
std::remove(
node.windows[i].tags.begin(),
node.windows[i].tags.end(),
"GUI"
),
node.windows[i].tags.end()
);
// If "show UI on first window" option is enabled, then modify the settings
// depending on if this is the first window or not
if (_settingsWidget->showUiOnFirstWindow()) {
if (i == 0) {
node.windows[i].viewports.back().isTracked = false;
node.windows[i].tags.push_back("GUI");
}
node.windows[i].draw2D = (i == 0);
node.windows[i].draw3D = (i != 0);
}
}
}
sgct::config::Cluster SgctEdit::cluster() const {

View File

@@ -43,15 +43,22 @@ namespace {
"Primary", "Secondary", "Tertiary", "Quaternary"
};
constexpr int nQualityTypes = 10;
const QList<QString> QualityTypes = {
"Low (256)", "Medium (512)", "High (1K)", "1.5K (1536)", "2K (2048)", "4K (4096)",
"8K (8192)", "16K (16384)", "32K (32768)", "64K (65536)"
};
constexpr int QualityValues[10] = {
constexpr int QualityValues[nQualityTypes] = {
256, 512, 1024, 1536, 2048, 4096, 8192, 16384, 32768, 65536
};
const QList<QString> ProjectionTypes = {
"Planar Projection", "Fisheye", "Spherical Mirror Projection",
"Cylindrical Projection", "Equirectangular Projection"
};
constexpr std::array<QRectF, 4> DefaultWindowSizes = {
QRectF(50.f, 50.f, 1280.f, 720.f),
QRectF(150.f, 150.f, 1280.f, 720.f),
@@ -59,7 +66,7 @@ namespace {
QRectF(150.f, 150.f, 1280.f, 720.f)
};
constexpr int LineEditWidthFixedWindowSize = 50;
constexpr int LineEditWidthFixedWindowSize = 64;
constexpr float DefaultFovLongEdge = 80.f;
constexpr float DefaultFovShortEdge = 50.534f;
constexpr float DefaultHeightOffset = 0.f;
@@ -81,7 +88,7 @@ namespace {
WindowControl::WindowControl(int monitorIndex, int windowIndex,
const std::vector<QRect>& monitorDims,
const QColor& winColor, QWidget* parent)
const QColor& winColor, bool resetToDefault, QWidget* parent)
: QWidget(parent)
, _monitorIndexDefault(monitorIndex)
, _windowIndex(windowIndex)
@@ -90,7 +97,9 @@ WindowControl::WindowControl(int monitorIndex, int windowIndex,
, _unlockIcon(":/images/outline_unlocked.png")
{
createWidgets(winColor);
resetToDefaults();
if (resetToDefault) {
resetToDefaults();
}
}
void WindowControl::createWidgets(const QColor& windowColor) {
@@ -293,11 +302,11 @@ void WindowControl::createWidgets(const QColor& windowColor) {
_projectionType = new QComboBox;
_projectionType->addItems({
"Planar Projection",
"Fisheye",
"Spherical Mirror Projection",
"Cylindrical Projection",
"Equirectangular Projection"
ProjectionTypes[0],
ProjectionTypes[1],
ProjectionTypes[2],
ProjectionTypes[3],
ProjectionTypes[4]
});
_projectionType->setToolTip("Select from the supported window projection types");
_projectionType->setCurrentIndex(0);
@@ -625,10 +634,30 @@ void WindowControl::resetToDefaults() {
emit windowChanged(_monitorIndexDefault, _windowIndex, _windowDimensions);
}
void WindowControl::setDimensions(QRectF newDims) {
_windowDimensions = newDims;
_sizeX->setValue(_windowDimensions.width());
_sizeY->setValue(_windowDimensions.height());
_offsetX->setValue(_windowDimensions.x());
_offsetY->setValue(_windowDimensions.y());
}
void WindowControl::setMonitorSelection(int monitorIndex) {
_monitor->setCurrentIndex(monitorIndex);
}
void WindowControl::showWindowLabel(bool show) {
_windowNumber->setVisible(show);
}
void WindowControl::setWindowName(const std::string& windowName) {
_windowName->setText(QString::fromStdString(windowName));
}
void WindowControl::setDecorationState(bool hasWindowDecoration) {
_windowDecoration->setChecked(hasWindowDecoration);
}
sgct::config::Projections WindowControl::generateProjectionInformation() const {
ProjectionIndices type =
static_cast<WindowControl::ProjectionIndices>(_projectionType->currentIndex());
@@ -706,9 +735,9 @@ sgct::config::Projections WindowControl::generateProjectionInformation() const {
}
}
sgct::config::Window WindowControl::generateWindowInformation() const {
sgct::config::Window window;
window.size = sgct::ivec2(_sizeX->text().toInt(), _sizeY->text().toInt());
void WindowControl::generateWindowInformation(sgct::config::Window& window) const {
window.size = { _sizeX->text().toInt(), _sizeY->text().toInt() };
window.monitor = _monitor->currentIndex();
QRect resolution = _monitorResolutions[_monitor->currentIndex()];
window.pos = sgct::ivec2(
resolution.x() + _offsetX->text().toInt(),
@@ -720,17 +749,56 @@ sgct::config::Window WindowControl::generateWindowInformation() const {
vp.position = sgct::vec2(0.f, 0.f);
vp.size = sgct::vec2(1.f, 1.f);
vp.projection = generateProjectionInformation();
window.viewports.clear();
window.viewports.push_back(vp);
window.isDecorated = _windowDecoration->isChecked();
if (window.isFullScreen) {
window.monitor = _monitor->currentIndex();
}
if (!_windowName->text().isEmpty()) {
window.name = _windowName->text().toStdString();
}
return window;
}
void WindowControl::setProjectionPlanar(float hfov, float vfov) {
_planar.fovH->setValue(hfov);
_planar.fovV->setValue(vfov);
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Planar));
}
void WindowControl::setProjectionFisheye(int quality, bool spoutOutput) {
setQualityComboBoxFromLinesResolution(quality, _fisheye.quality);
_fisheye.spoutOutput->setChecked(spoutOutput);
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Fisheye));
}
void WindowControl::setProjectionSphericalMirror(int quality) {
setQualityComboBoxFromLinesResolution(quality, _sphericalMirror.quality);
_projectionType->setCurrentIndex(
static_cast<int>(ProjectionIndices::SphericalMirror)
);
}
void WindowControl::setProjectionCylindrical(int quality, float heightOffset) {
setQualityComboBoxFromLinesResolution(quality, _cylindrical.quality);
_cylindrical.heightOffset->setValue(heightOffset);
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Cylindrical));
}
void WindowControl::setProjectionEquirectangular(int quality, bool spoutOutput) {
setQualityComboBoxFromLinesResolution(quality, _equirectangular.quality);
_equirectangular.spoutOutput->setChecked(spoutOutput);
_projectionType->setCurrentIndex(
static_cast<int>(ProjectionIndices::Equirectangular)
);
}
void WindowControl::setQualityComboBoxFromLinesResolution(int lines, QComboBox* combo) {
ghoul_assert(combo, "Invalid pointer");
for (unsigned int v = 0; v < nQualityTypes; ++v) {
if (lines == QualityValues[v]) {
combo->setCurrentIndex(v);
break;
}
}
}
void WindowControl::onSizeXChanged(int newValue) {

View File

@@ -0,0 +1,322 @@
{
"$id": "schema2e",
"$defs": {
"cylindricalprojection": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "CylindricalProjection",
"default": "CylindricalProjection",
"readOnly": true
},
"quality": {
"$ref": "sgct.schema.json#/$defs/projectionquality"
},
"heightOffset": {
"type": "number",
"title": "Height Offset",
"description": "Offsets the height from which the cylindrical projection is generated. This is, in general, only necessary if the user position is offset and you want to counter that offset to continue producing a “standard” cylindrical projection"
}
},
"required": [ "type" ],
"additionalProperties": false,
"description": "This projection method renders the scene into a view that can be mapped on the inside or outside of a cylinder. This projection method is support by some live media curation tools. The forward-facing direction will be at the left border of the image unless changed via the rotation option"
},
"fisheyeprojection": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "FisheyeProjection",
"default": "FisheyeProjection",
"readOnly": true
},
"quality": {
"$ref": "sgct.schema.json#/$defs/projectionquality"
}
},
"required": [ "type" ],
"additionalProperties": false,
"description": "Describes a fisheye projection that is used to render into its parent Viewport. This uses a default of 180 degrees field of view and has a 1:1 aspect ratio. This projection type counts as a non-linear projection, which requires 4-6 render passes of the application, meaning that the application might render slower when using these kind of projections than a flat projection. In either case, the application does not need to be aware of the projection as this abstract is handled internally and the applications draw method is only called multiple times per frame with different projection methods that are used to create the full fisheye projection"
},
"planarprojection": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "PlanarProjection",
"default": "PlanarProjection",
"readOnly": true
},
"fov": {
"$ref": "sgct.schema.json#/$defs/fovhorizontalvertical",
"title": "Camera Field-of-View",
"description": "This element describes the field of view used the camera in this planar projection"
}
},
"additionalProperties": false,
"description": "Describes a projection for the Viewport that is a flat projection described by simple frustum, which may be asymmetric"
},
"sphericalmirrorprojection": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "SphericalMirrorProjection",
"default": "SphericalMirrorProjection",
"readOnly": true
},
"quality": {
"$ref": "sgct.schema.json#/$defs/projectionquality"
}
},
"required": [ "type" ],
"additionalProperties": false,
"description": "Used to create a projection used for Paul Bourke's spherical mirror setup (see here), which makes it possible to use an off-the-shelf projector to create a planetarium-like environment by bouncing the image of a shiny metal mirror. Please note that this is not the only way to produce these kind of images. Depending on your setup and availability of warping meshes, it might suffice to use the FisheyeProjection node type instead and add a single mesh to the parent Viewport instead. The config folder in SGCT contains an example of this using a default 16x9 warping mesh. This projection type specifically deals with the case where you have four different meshes, one for the bottom, top, left, and right parts of the distorted image"
},
"spoutoutputprojection": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "SpoutOutputProjection",
"default": "SpoutOutputProjection",
"readOnly": true
},
"quality": {
"$ref": "sgct.schema.json#/$defs/projectionquality"
},
"mapping": {
"type": "string",
"enum": [ "fisheye", "equirectangular" ],
"title": "Mapping",
"description": "Determines the type of sharing that occurs with this projection and thus how many and which texture is shared via Spout. For the “fisheye” and “equirectangular”, only the single, final reprojected image is shared, for the “cubemap” method, all selected cubemaps will be provided through the Spout interface. The default value is “cubemap”"
},
"mappingspoutname": {
"type": "string",
"title": "Mapping Spout Name",
"description": "Sets the name of the texture if the mapping type is 'fisheye' or 'equirectangular'. If the mapping is 'cubemap', this value is ignored"
}
},
"required": [ "type", "mapping" ],
"additionalProperties": false,
"description": "Provides the ability to share a fully reprojected image using the Spout library. This library only supports the Windows operating system, so this projection will only work on Windows machines. Spout's functionality is the abilty to shared textures between different applications on the same machine, making it possible to render images using SGCT and making them available to other real-time applications on the same machine for further processing. Spout uses a textual name for accessing which texture should be used for sharing. The SpoutOutputProjection has three different output types, outputting each cube map face, sharing a fisheye image, or sharing an equirectangular projection, as determined by the mapping attribute"
},
"projection": {
"oneOf": [
{
"$ref": "#/$defs/planarprojection",
"title": "Planar Projection"
},
{
"$ref": "#/$defs/fisheyeprojection",
"title": "Fisheye Projection"
},
{
"$ref": "#/$defs/sphericalmirrorprojection",
"title": "Spherical Mirror Projection"
},
{
"$ref": "#/$defs/spoutoutputprojection",
"title": "Spout Output Projection"
},
{
"$ref": "#/$defs/cylindricalprojection",
"title": "Cylindrical Projection"
},
{
"$ref": "sgct.schema.json#/$defs/equirectangularprojection",
"title": "Equirectangular Projection"
}
],
"title": "Projection"
},
"node": {
"type": "object",
"properties": {
"address": {
"$ref": "sgct.schema.json#/$defs/address"
},
"port": {
"$ref": "sgct.schema.json#/$defs/port"
},
"windows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"border": {
"$ref": "sgct.schema.json#/$defs/windowborder"
},
"draw2d": {
"$ref": "sgct.schema.json#/$defs/draw2d"
},
"draw3d": {
"$ref": "sgct.schema.json#/$defs/draw3d"
},
"monitor": {
"$ref": "sgct.schema.json#/$defs/monitor"
},
"id": {
"$ref": "sgct.schema.json#/$defs/id"
},
"name": {
"$ref": "sgct.schema.json#/$defs/windowname"
},
"pos": {
"$ref": "sgct.schema.json#/$defs/windowpos"
},
"size": {
"$ref": "sgct.schema.json#/$defs/windowsize"
},
"tags": {
"$ref": "sgct.schema.json#/$defs/tags"
},
"viewports": {
"type": "array",
"items": {
"type": "object",
"properties": {
"pos": {
"$ref": "sgct.schema.json#/$defs/viewportpos"
},
"size": {
"$ref": "sgct.schema.json#/$defs/viewportsize"
},
"projection": {
"$ref": "sgct.schema.json#/$defs/projection"
},
"tracked": {
"$ref": "sgct.schema.json#/$defs/tracked"
}
}
},
"title": "Viewports"
}
},
"required": [ "pos", "size", "viewports" ]
},
"title": "Windows",
"description": "Specifies a single window that is used to render content into. There can be an arbitrary(-ish) number of windows for each node and they all will be created and initialized at start time. Each window has at least one Viewport that specifies exactly where in the window the rendering occurs with which parameters"
}
},
"required": [ "address", "port", "windows" ],
"additionalProperties": false,
"description": "Defines a single computing node that is contained in the described cluster. In general this corresponds to a single computer, but it is also possible to create multiple nodes on a local machine by using the 127.0.0.x IP address with x from 0 to 255. It is not possible to create multiple nodes on the same remote computer, however"
},
"scene": {
"type": "object",
"properties": {
"orientation": {
"$ref": "sgct.schema.json#/$defs/quat",
"title": "Orientation",
"description": "Describes a fixed orientation of the global scene"
}
},
"required": [ "orientation" ],
"additionalProperties": false,
"description": "Determines an overall orientation of the scene. It consists only of an Orientation, which is included in the projection matrix that is passed to the rendering function callback of the specific application. This node can be used to customize the rendering for a specific rendering window. A common use-case in planetariums, for example, is to account for a tilt in the display system by providing an Orientation with the same pitch as the planetarium surface. This makes it possible to reuse the same application between the planetarium dome and fixed setups without the need for special care"
},
"display": {
"type": "object",
"properties": {
"swapinterval": {
"type": "integer",
"minimum": 0,
"title": "Swap Interval",
"description": "Determines the swap interval for the application. This determines the amount of V-Sync that should occur for the application. The two most common values for this are 0 for disabling V-Sync and 1 for regular V-Sync. The number provided determines the number of screen updates to wait before swapping the backbuffers and returning. For example on a 60Hz monitor, swapinterval=\"1\" would lead to a maximum of 60Hz frame rate, swapinterval=\"2\" would lead to a maximum of 30Hz frame rate. Using the same values for a 144Hz monitor would be a refresh rate of 144 and 72 respectively. The default value is 0, meaning that V-Sync is disabled"
}
},
"title": "Display",
"additionalProperties": false,
"description": "Settings specific for the handling of display-related settings for the whole application"
},
"settings": {
"type": "object",
"properties": {
"display": {
"$ref": "#/$defs/display"
}
},
"additionalProperties": false,
"description": "Controls global settings that affect the overall behavior of the SGCT library that are not limited just to a single window"
},
"user": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Name",
"description": "Specifies the name of this user. Each user needs to have a unique name, but there also has to be exactly one user present that has an empty name (or without a name attribute) which is used as the default user"
},
"eyeseparation": {
"type": "number",
"minimum": 0.0,
"title": "Eye Separation",
"description": "Determines the eye separation used for stereoscopic viewports. If no viewports in the configuration are using stereo, this setting is ignored"
},
"pos": {
"$ref": "sgct.schema.json#/$defs/vec3",
"title": "Position",
"description": "A linear offset of the user position. Must define three float attributes x, y, and z. The default values are x=0, y=0, z=0, meaning that no linear offset is applied to the user's position"
}
},
"additionalProperties": false,
"required": [ "pos" ],
"description": "Specifies a user position and parameters. In most cases, only a single unnamed user is necessary. However, in more general cases, it is possible to assign Users to specific Viewports to provide a more fine-grained control over the rendering that occurs in that viewport"
}
},
"type": "object",
"properties": {
"masteraddress": {
"$ref": "sgct.schema.json#/$defs/masteraddress"
},
"nodes": {
"type": "array",
"items": { "$ref": "#/$defs/node" },
"title": "Nodes"
},
"scene": {
"$ref": "#/$defs/scene",
"title": "Scene"
},
"settings": {
"$ref": "#/$defs/settings",
"title": "Settings"
},
"version": {
"type": "integer"
},
"users": {
"type": "array",
"items": { "$ref": "#/$defs/user" },
"title": "Users"
},
"generator": {
"type": "object",
"properties": {
"name": { "type": "string" },
"major": { "type": "integer" },
"minor": { "type": "integer" }
},
"required": [ "name", "major", "minor" ]
}
},
"additionalProperties": false,
"required": [
"version", "masteraddress", "scene", "users", "generator"
]
}

View File

@@ -47,6 +47,7 @@ struct Configuration {
std::string windowConfiguration = "${CONFIG}/single.xml";
std::string asset;
std::string profile;
std::vector<std::string> readOnlyWindowConfigs;
std::vector<std::string> readOnlyProfiles;
std::vector<std::string> globalCustomizationScripts;
std::map<std::string, std::string> pathTokens = {

View File

@@ -59,6 +59,27 @@ SGCTConfig = sgct.config.single{vsync=false}
-- spout_output_fisheye.json: a window where the rendering is sent to spout (fisheye
-- output) instead of the display
-- These are window configurations that are set to "read-only"
ReadOnlyWindowConfigs = {
"equirectangular_gui.json",
"fullscreen1080.json",
"gui_projector.json",
"single_fisheye_gui.json",
"single_fisheye.json",
"single_gui.json",
"single_gui_spout.json",
"single.json",
"single_sbs_stereo.json",
"single_two_win.json",
"spherical_mirror_gui.json",
"spherical_mirror.json",
"spout_output_cubemap.json",
"spout_output_equirectangular.json",
"spout_output_fisheye.json",
"spout_output_flat.json",
"two_nodes.json"
}
-- Variable: Profile
-- Sets the profile that should be loaded by OpenSpace.
Profile = "default"

View File

@@ -291,6 +291,9 @@ namespace {
// displayed while the scene graph is created and initialized
std::optional<LoadingScreen> loadingScreen;
// List of window configurations that cannot be overwritten by user
std::optional<std::vector<std::string>> readOnlyWindowConfigs;
// List of profiles that cannot be overwritten by user
std::optional<std::vector<std::string>> readOnlyProfiles;
@@ -438,6 +441,7 @@ void parseLuaState(Configuration& configuration) {
c.httpProxy.password = p.httpProxy->password.value_or(c.httpProxy.password);
}
c.readOnlyWindowConfigs = p.readOnlyWindowConfigs.value_or(c.readOnlyWindowConfigs);
c.readOnlyProfiles = p.readOnlyProfiles.value_or(c.readOnlyProfiles);
c.bypassLauncher = p.bypassLauncher.value_or(c.bypassLauncher);
}

View File

@@ -39,6 +39,7 @@ add_executable(
test_profile.cpp
test_rawvolumeio.cpp
test_scriptscheduler.cpp
test_sgctedit.cpp
test_spicemanager.cpp
test_timeconversion.cpp
test_timeline.cpp
@@ -53,8 +54,14 @@ add_executable(
set_openspace_compile_settings(OpenSpaceTest)
target_include_directories(OpenSpaceTest
PUBLIC
"../apps/OpenSpace/ext/sgct/ext/json/include"
"../apps/OpenSpace/ext/sgct/ext/json-schema-validator/src"
)
target_compile_definitions(OpenSpaceTest PUBLIC "GHL_THROW_ON_ASSERT")
target_link_libraries(OpenSpaceTest PUBLIC Catch2 openspace-core)
target_link_libraries(OpenSpaceTest PUBLIC Catch2 openspace-core sgct)
foreach (library_name ${all_enabled_modules})
get_target_property(library_type ${library_name} TYPE)

View File

@@ -0,0 +1,67 @@
{
"generator": {
"major": 0,
"minor": 11,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "min name",
"pos": {
"x": 10,
"y": 10
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
}

619
tests/test_sgctedit.cpp Normal file
View File

@@ -0,0 +1,619 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2023 *
* *
* 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 <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_exception.hpp>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/fmt.h>
#include <nlohmann/json.hpp>
#include <nlohmann/json-schema.hpp>
#include <sgct/readconfig.h>
#include <openspace/engine/configuration.h>
#include <filesystem>
#include <fstream>
using namespace openspace::configuration;
namespace {
std::string stringify(const std::string filename) {
std::ifstream myfile;
myfile.open(filename);
std::stringstream buffer;
buffer << myfile.rdbuf();
return buffer.str();
}
void attemptValidation(const std::string cfgString) {
std::filesystem::path schemaDir = absPath("${TESTDIR}/../config/schema");
std::string schemaString = stringify(
schemaDir.string() + "/sgcteditor.schema.json"
);
sgct::validateConfigAgainstSchema(cfgString, schemaString, schemaDir);
}
} // namespace
TEST_CASE("SgctEdit: pass", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 1,
"minor": 1,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "ffss",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
})";
CHECK_NOTHROW(attemptValidation(config));
}
TEST_CASE("SgctEdit: addedTrailingBracket", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 0,
"minor": 1,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "ffss",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
}})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
nlohmann::json::parse_error,
Catch::Matchers::Message(
"[json.exception.parse_error.101] parse error at line 67, column 2: "
"syntax error while parsing value - unexpected '}'; expected "
"end of input"
)
);
}
TEST_CASE("SgctEdit: missingMasterAddress", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 1,
"minor": 1,
"name": "SgctWindowConfig"
},
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 1,
"name": "name",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
std::exception,
Catch::Matchers::Message(
"At of {\"generator\":{\"major\":1,\"minor\":1,\"name\":"
"\"SgctWindowConfig\"},\"nodes\":[{\"address\":\"localhost\",\"port\":"
"20401,\"windows\":[{\"border\":true,\"id\":0,\"monitor\":1,\"name\":"
"\"name\",\"pos\":{\"x\":112,\"y\":77},\"size\":{\"x\":1280,\"y\":720},"
"\"viewports\":[{\"pos\":{\"x\":0.0,\"y\":0.0},\"projection\":"
"{\"heightoffset\":0.0,\"quality\":\"1024\",\"type\":"
"\"CylindricalProjection\"},\"size\":{\"x\":1.0,\"y\":1.0},\"tracked\":"
"true}]}]}],\"scene\":{\"orientation\":{\"w\":0.0,\"x\":0.0,\"y\":0.0,"
"\"z\":0.0}},\"users\":[{\"eyeseparation\":0.06499999761581421,\"pos\":"
"{\"x\":0.0,\"y\":0.0,\"z\":4.0}}],\"version\":1} - required property "
"'masteraddress' not found in object\n"
)
);
}
TEST_CASE("SgctEdit: missingPos", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 1,
"minor": 1,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "name",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421
}
],
"version": 1
})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
std::exception,
Catch::Matchers::Message(
"At /users/0 of {\"eyeseparation\":0.06499999761581421} - required "
"property 'pos' not found in object\n"
)
);
}
TEST_CASE("SgctEdit: missingGenerator", "[sgctedit]") {
const std::string config =
R"({
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "name",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
std::exception,
Catch::Matchers::Message(
"At of {\"masteraddress\":\"localhost\",\"nodes\":[{\"address\":"
"\"localhost\",\"port\":20401,\"windows\":[{\"border\":true,\"id\":"
"0,\"monitor\":0,\"name\":\"name\",\"pos\":{\"x\":112,\"y\":77},\"size\":"
"{\"x\":1280,\"y\":720},\"viewports\":[{\"pos\":{\"x\":0.0,\"y\":0.0},"
"\"projection\":{\"heightoffset\":0.0,\"quality\":\"1024\",\"type\":"
"\"CylindricalProjection\"},\"size\":{\"x\":1.0,\"y\":1.0},\"tracked\":"
"true}]}]}],\"scene\":{\"orientation\":{\"w\":0.0,\"x\":0.0,\"y\":0.0,\"z\":"
"0.0}},\"users\":[{\"eyeseparation\":0.06499999761581421,\"pos\":{\"x\":"
"0.0,\"y\":0.0,\"z\":4.0}}],\"version\":1} - required property 'generator' "
"not found in object\n"
)
);
}
TEST_CASE("SgctEdit: minimumVersion", "[sgctedit]") {
const sgct::config::GeneratorVersion minVersion { "SgctWindowConfig", 1, 1 };
std::string inputCfg =
absPath("${TESTDIR}/sgctedit/fails_minimum_version.json").string();
sgct::config::GeneratorVersion ver = sgct::readConfigGenerator(inputCfg);
CHECK_FALSE(ver.versionCheck(minVersion));
}
TEST_CASE("SgctEdit: invalidZvalue", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 1,
"minor": 1,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 1,
"name": "ffss",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720,
"z": s
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"eyeseparation": 0.06499999761581421,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
std::exception,
Catch::Matchers::Message(
"[json.exception.parse_error.101] parse error at line 25, column 11: "
"syntax error while parsing value - invalid literal; last read: '\"z\": s'"
)
);
}
TEST_CASE("SgctEdit: unwelcomeValue", "[sgctedit]") {
const std::string config =
R"({
"generator": {
"major": 1,
"minor": 1,
"name": "SgctWindowConfig"
},
"masteraddress": "localhost",
"nodes": [
{
"address": "localhost",
"port": 20401,
"windows": [
{
"border": true,
"id": 0,
"monitor": 0,
"name": "ffss",
"pos": {
"x": 112,
"y": 77
},
"size": {
"x": 1280,
"y": 720
},
"viewports": [
{
"pos": {
"x": 0.0,
"y": 0.0
},
"projection": {
"heightoffset": 0.0,
"quality": "1024",
"type": "CylindricalProjection"
},
"size": {
"x": 1.0,
"y": 1.0
},
"tracked": true
}
]
}
]
}
],
"scene": {
"orientation": {
"w": 0.0,
"x": 0.0,
"y": 0.0,
"z": 0.0
}
},
"users": [
{
"extra": "???",
"eyeseparation": 0.6,
"pos": {
"x": 0.0,
"y": 0.0,
"z": 4.0
}
}
],
"version": 1
})";
CHECK_THROWS_MATCHES(
attemptValidation(config),
std::exception,
Catch::Matchers::Message(
"At /users/0 of {\"extra\":\"???\",\"eyeseparation\":0.6,\"pos\":"
"{\"x\":0.0,\"y\":0.0,\"z\":4.0}} - validation failed for additional "
"property 'extra': instance invalid as per false-schema\n"
)
);
}