diff --git a/apps/OpenSpace/CMakeLists.txt b/apps/OpenSpace/CMakeLists.txt index 94ecb33a82..e5f1ee78f4 100644 --- a/apps/OpenSpace/CMakeLists.txt +++ b/apps/OpenSpace/CMakeLists.txt @@ -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") diff --git a/apps/OpenSpace/ext/launcher/include/launcherwindow.h b/apps/OpenSpace/ext/launcher/include/launcherwindow.h index 2f1ca6b732..b0965a3231 100644 --- a/apps/OpenSpace/ext/launcher/include/launcherwindow.h +++ b/apps/OpenSpace/ext/launcher/include/launcherwindow.h @@ -29,6 +29,8 @@ #include "sgctedit/sgctedit.h" #include +#include +#include #include #include @@ -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& _readOnlyWindowConfigs; const std::vector& _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__ diff --git a/apps/OpenSpace/ext/launcher/include/sgctedit/displaywindowunion.h b/apps/OpenSpace/ext/launcher/include/sgctedit/displaywindowunion.h index ea64258da9..d4d67968c3 100644 --- a/apps/OpenSpace/ext/launcher/include/sgctedit/displaywindowunion.h +++ b/apps/OpenSpace/ext/launcher/include/sgctedit/displaywindowunion.h @@ -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& monitorSizeList, - int nMaxWindows, const std::array& windowColors, + int nMaxWindows, const std::array& windowColors, bool resetToDefault, QWidget* parent = nullptr); /** @@ -59,7 +61,15 @@ public: * * \return vector of pointers of WindowControl objects */ - std::vector windowControls() const; + std::vector 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& 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 monitorResolutions, - std::array windowColors); + std::array windowColors, bool resetToDefault); void showWindows(); unsigned int _nWindowsDisplayed = 0; diff --git a/apps/OpenSpace/ext/launcher/include/sgctedit/settingswidget.h b/apps/OpenSpace/ext/launcher/include/sgctedit/settingswidget.h index 1ff9fd8578..b20a11a1f3 100644 --- a/apps/OpenSpace/ext/launcher/include/sgctedit/settingswidget.h +++ b/apps/OpenSpace/ext/launcher/include/sgctedit/settingswidget.h @@ -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; diff --git a/apps/OpenSpace/ext/launcher/include/sgctedit/sgctedit.h b/apps/OpenSpace/ext/launcher/include/sgctedit/sgctedit.h index b3a0e7983d..17918e320c 100644 --- a/apps/OpenSpace/ext/launcher/include/sgctedit/sgctedit.h +++ b/apps/OpenSpace/ext/launcher/include/sgctedit/sgctedit.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -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& configsReadOnly, + QWidget* parent); + /** * Returns the saved filename * @@ -66,8 +87,16 @@ public: sgct::config::Cluster cluster() const; private: - void createWidgets(const std::vector& monitorSizes); - sgct::config::Cluster generateConfiguration() const; + std::vector createMonitorInfoSet(); + void createWidgets(const std::vector& 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 _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__ diff --git a/apps/OpenSpace/ext/launcher/include/sgctedit/windowcontrol.h b/apps/OpenSpace/ext/launcher/include/sgctedit/windowcontrol.h index 71f155823c..13065b5c54 100644 --- a/apps/OpenSpace/ext/launcher/include/sgctedit/windowcontrol.h +++ b/apps/OpenSpace/ext/launcher/include/sgctedit/windowcontrol.h @@ -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& monitorDims, const QColor& winColor, QWidget* parent); + const std::vector& 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; diff --git a/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp b/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp index 9f379e3d63..c7334ab384 100644 --- a/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp +++ b/apps/OpenSpace/ext/launcher/src/launcherwindow.cpp @@ -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::of(&QComboBox::currentIndexChanged), + nullptr, + nullptr + ); _windowConfigBox->clear(); _userConfigCount = 0; + _userConfigStartingIdx = 0; + _preDefinedConfigStartingIdx = 0; _windowConfigBox->addItem(QString::fromStdString("--- User Configurations ---")); const QStandardItemModel* model = qobject_cast(_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(_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::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); +} diff --git a/apps/OpenSpace/ext/launcher/src/sgctedit/displaywindowunion.cpp b/apps/OpenSpace/ext/launcher/src/sgctedit/displaywindowunion.cpp index 0f420d9de1..361004653a 100644 --- a/apps/OpenSpace/ext/launcher/src/sgctedit/displaywindowunion.cpp +++ b/apps/OpenSpace/ext/launcher/src/sgctedit/displaywindowunion.cpp @@ -36,16 +36,22 @@ DisplayWindowUnion::DisplayWindowUnion(const std::vector& monitorSizeList, int nMaxWindows, const std::array& 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 monitorResolutions, - std::array windowColors) + std::array 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 DisplayWindowUnion::windowControls() const { +std::vector DisplayWindowUnion::activeWindowControls() const { std::vector res; res.reserve(_nWindowsDisplayed); for (unsigned int i = 0; i < _nWindowsDisplayed; ++i) { @@ -129,6 +136,10 @@ std::vector DisplayWindowUnion::windowControls() const { return res; } +std::vector& 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); diff --git a/apps/OpenSpace/ext/launcher/src/sgctedit/settingswidget.cpp b/apps/OpenSpace/ext/launcher/src/sgctedit/settingswidget.cpp index 99ee2a9f6a..6240f68f0a 100644 --- a/apps/OpenSpace/ext/launcher/src/sgctedit/settingswidget.cpp +++ b/apps/OpenSpace/ext/launcher/src/sgctedit/settingswidget.cpp @@ -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); +} diff --git a/apps/OpenSpace/ext/launcher/src/sgctedit/sgctedit.cpp b/apps/OpenSpace/ext/launcher/src/sgctedit/sgctedit.cpp index f896a14800..e53e94bbba 100644 --- a/apps/OpenSpace/ext/launcher/src/sgctedit/sgctedit.cpp +++ b/apps/OpenSpace/ext/launcher/src/sgctedit/sgctedit.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -65,15 +64,160 @@ namespace { // We got to the end without running into any problems, so we are golden return false; } + + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; } // namespace SgctEdit::SgctEdit(QWidget* parent, std::string userConfigPath) : QDialog(parent) , _userConfigPath(std::move(userConfigPath)) { - QList 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& 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 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(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 SgctEdit::createMonitorInfoSet() { + QList screens = qApp->screens(); int nScreensManaged = std::min(static_cast(screens.length()), 4); std::vector monitorSizes; for (int s = 0; s < nScreensManaged; ++s) { @@ -88,11 +232,12 @@ SgctEdit::SgctEdit(QWidget* parent, std::string userConfigPath) static_cast(actualHeight * screens[s]->devicePixelRatio()) ); } - - createWidgets(monitorSizes); + return monitorSizes; } -void SgctEdit::createWidgets(const std::vector& monitorSizes) { +void SgctEdit::createWidgets(const std::vector& 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& monitorSizes) { monitorSizes, MaxNumberWindows, _colorsForWindows, + setToDefaults, this ); connect( @@ -128,7 +274,9 @@ void SgctEdit::createWidgets(const std::vector& 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& 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 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 { diff --git a/apps/OpenSpace/ext/launcher/src/sgctedit/windowcontrol.cpp b/apps/OpenSpace/ext/launcher/src/sgctedit/windowcontrol.cpp index a466f450af..c2d6c8cb32 100644 --- a/apps/OpenSpace/ext/launcher/src/sgctedit/windowcontrol.cpp +++ b/apps/OpenSpace/ext/launcher/src/sgctedit/windowcontrol.cpp @@ -43,15 +43,22 @@ namespace { "Primary", "Secondary", "Tertiary", "Quaternary" }; + constexpr int nQualityTypes = 10; + const QList 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 ProjectionTypes = { + "Planar Projection", "Fisheye", "Spherical Mirror Projection", + "Cylindrical Projection", "Equirectangular Projection" + }; + constexpr std::array 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& 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(_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(ProjectionIndices::Planar)); +} + +void WindowControl::setProjectionFisheye(int quality, bool spoutOutput) { + setQualityComboBoxFromLinesResolution(quality, _fisheye.quality); + _fisheye.spoutOutput->setChecked(spoutOutput); + _projectionType->setCurrentIndex(static_cast(ProjectionIndices::Fisheye)); +} + +void WindowControl::setProjectionSphericalMirror(int quality) { + setQualityComboBoxFromLinesResolution(quality, _sphericalMirror.quality); + _projectionType->setCurrentIndex( + static_cast(ProjectionIndices::SphericalMirror) + ); +} + +void WindowControl::setProjectionCylindrical(int quality, float heightOffset) { + setQualityComboBoxFromLinesResolution(quality, _cylindrical.quality); + _cylindrical.heightOffset->setValue(heightOffset); + _projectionType->setCurrentIndex(static_cast(ProjectionIndices::Cylindrical)); +} + +void WindowControl::setProjectionEquirectangular(int quality, bool spoutOutput) { + setQualityComboBoxFromLinesResolution(quality, _equirectangular.quality); + _equirectangular.spoutOutput->setChecked(spoutOutput); + _projectionType->setCurrentIndex( + static_cast(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) { diff --git a/apps/OpenSpace/ext/sgct b/apps/OpenSpace/ext/sgct index 05ed52d053..56509e9477 160000 --- a/apps/OpenSpace/ext/sgct +++ b/apps/OpenSpace/ext/sgct @@ -1 +1 @@ -Subproject commit 05ed52d0533b95954913a363499bb828512a6245 +Subproject commit 56509e947769e957b835196180dd9e75fe4d15be diff --git a/config/schema/sgcteditor.schema.json b/config/schema/sgcteditor.schema.json new file mode 100644 index 0000000000..7f0b2276d4 --- /dev/null +++ b/config/schema/sgcteditor.schema.json @@ -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" + ] +} diff --git a/include/openspace/engine/configuration.h b/include/openspace/engine/configuration.h index d63fbc5086..16cc54409d 100644 --- a/include/openspace/engine/configuration.h +++ b/include/openspace/engine/configuration.h @@ -47,6 +47,7 @@ struct Configuration { std::string windowConfiguration = "${CONFIG}/single.xml"; std::string asset; std::string profile; + std::vector readOnlyWindowConfigs; std::vector readOnlyProfiles; std::vector globalCustomizationScripts; std::map pathTokens = { diff --git a/openspace.cfg b/openspace.cfg index 5ff6679817..6806e32345 100644 --- a/openspace.cfg +++ b/openspace.cfg @@ -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" diff --git a/src/engine/configuration.cpp b/src/engine/configuration.cpp index 1712c61b02..0452b32bc8 100644 --- a/src/engine/configuration.cpp +++ b/src/engine/configuration.cpp @@ -291,6 +291,9 @@ namespace { // displayed while the scene graph is created and initialized std::optional loadingScreen; + // List of window configurations that cannot be overwritten by user + std::optional> readOnlyWindowConfigs; + // List of profiles that cannot be overwritten by user std::optional> 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); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b2d5e9f9cb..7aa711c631 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/sgctedit/fails_minimum_version.json b/tests/sgctedit/fails_minimum_version.json new file mode 100644 index 0000000000..501ecae40a --- /dev/null +++ b/tests/sgctedit/fails_minimum_version.json @@ -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 +} diff --git a/tests/test_sgctedit.cpp b/tests/test_sgctedit.cpp new file mode 100644 index 0000000000..ea224bf9c0 --- /dev/null +++ b/tests/test_sgctedit.cpp @@ -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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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" + ) + ); +}