Launcher improvements (#3523)

- Adding support for subfolders in configurations and profiles
  - Disable the edit button if profile does not exist
  - Disable the Start button if the profile or window configuration does not exist
  - Change the layout of the Window Configuration editor
    - Explicitly specify which windows should have Overlay rendering and 3D rendering
    - Add keyboard shortcuts for adding and removing windows
  - Use a normal Save dialog for the profile editor instead of the renaming + Duplicate button
This commit is contained in:
Alexander Bock
2025-02-24 16:19:42 +01:00
committed by GitHub
parent 4d7e5c2cb5
commit 61ed1f4c9b
45 changed files with 1785 additions and 2431 deletions

View File

@@ -25,9 +25,12 @@
include(${PROJECT_SOURCE_DIR}/support/cmake/set_openspace_compile_settings.cmake)
set(HEADER_FILES
include/backgroundimage.h
include/filesystemaccess.h
include/launcherwindow.h
include/settingsdialog.h
include/splitcombobox.h
include/windowcolors.h
include/profile/actiondialog.h
include/profile/additionalscriptsdialog.h
include/profile/assetsdialog.h
@@ -47,16 +50,17 @@ set(HEADER_FILES
include/profile/propertiesdialog.h
include/sgctedit/displaywindowunion.h
include/sgctedit/monitorbox.h
include/sgctedit/orientationdialog.h
include/sgctedit/settingswidget.h
include/sgctedit/sgctedit.h
include/sgctedit/windowcontrol.h
)
set(SOURCE_FILES
src/backgroundimage.cpp
src/launcherwindow.cpp
src/filesystemaccess.cpp
src/settingsdialog.cpp
src/splitcombobox.cpp
src/windowcolors.cpp
src/profile/actiondialog.cpp
src/profile/additionalscriptsdialog.cpp
src/profile/assetsdialog.cpp
@@ -77,8 +81,6 @@ set(SOURCE_FILES
src/sgctedit/sgctedit.cpp
src/sgctedit/displaywindowunion.cpp
src/sgctedit/monitorbox.cpp
src/sgctedit/orientationdialog.cpp
src/sgctedit/settingswidget.cpp
src/sgctedit/windowcontrol.cpp
)

View File

@@ -22,35 +22,18 @@
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_UI_LAUNCHER___ORIENTATIONDIALOG___H__
#define __OPENSPACE_UI_LAUNCHER___ORIENTATIONDIALOG___H__
#ifndef __OPENSPACE_UI_LAUNCHER___BACKGROUNDIMAGE___H__
#define __OPENSPACE_UI_LAUNCHER___BACKGROUNDIMAGE___H__
#include <QDialog>
#include <QLabel>
#include <sgct/math.h>
#include <filesystem>
class QLineEdit;
class QWidget;
class OrientationDialog final : public QDialog {
class BackgroundImage final : public QLabel {
Q_OBJECT
public:
/**
* Constructor for OrientationDialog object which contains the input text boxes for
* orientation x,y,z values,
*
* \param orientation The x,y,z angles in degrees contained in sgct::quat object
* \param parent Pointer to Qt QWidget parent object
*/
OrientationDialog(sgct::quat& orientation, QWidget* parent);
private:
void ok();
QLineEdit* _linePitch = nullptr;
QLineEdit* _lineRoll = nullptr;
QLineEdit* _lineYaw = nullptr;
sgct::quat& _orientationValue;
BackgroundImage(QRect size, const std::filesystem::path& syncFolder,
QWidget* parent = nullptr);
};
#endif // __OPENSPACE_UI_LAUNCHER___ORIENTATIONDIALOG___H__
#endif // __OPENSPACE_UI_LAUNCHER___BACKGROUNDIMAGE___H__

View File

@@ -35,10 +35,9 @@
#include <filesystem>
#include <optional>
namespace openspace { struct Configuration; }
class SplitComboBox;
class QComboBox;
class QLabel;
namespace openspace { struct Configuration; }
class LauncherWindow final : public QMainWindow {
Q_OBJECT
@@ -51,11 +50,9 @@ public:
* \param sgctConfigEnabled `true` if window selection combo box will be enabled
* \param sgctConfigName The name of the sgct configuration function used to generate
* window config (blank if file is used)
* \param parent The parent that contains this (and possibly other) children in the
* tree structure.
*/
LauncherWindow(bool profileEnabled, const openspace::Configuration& globalConfig,
bool sgctConfigEnabled, std::string sgctConfigName, QWidget* parent);
bool sgctConfigEnabled, std::string sgctConfigName);
/**
* Returns whether "Start OpenSpace" was chosen when this window closed.
@@ -80,14 +77,6 @@ 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;
/**
* Handles keypresses while the Qt launcher window is open.
*
@@ -95,20 +84,42 @@ public:
*/
void keyPressEvent(QKeyEvent* evt) override;
private slots:
// Open the profile editor to the currently selected profile
void editProfile();
// Open the profile editor to a new empty profile
void newProfile();
// A new profile has been selected. In this case `selection` is guaranteed to have a
// value
void selectProfile(std::optional<std::string> selection);
// Open the window configuration on the currently selected file
void editConfiguration();
// Open the window configuration on a new empty configuration
void newConfiguration();
// A new configuration has been selected. `selection` might be `std::nullopt` if the
// newly selected configuration was the value provided in the openspace.cfg file
void selectConfiguration(std::optional<std::string> selection);
// The main start button has been pressed
void start();
// Open the settings dialog window
void openSettings();
private:
QWidget* createCentralWidget();
void setBackgroundImage(const std::filesystem::path& syncPath);
// Actually open the profile editor
void openProfileEditor(const std::string& profile, bool isUserProfile);
void openWindowEditor(const std::string& winCfg, bool isUserWinCfg);
void editRefusalDialog(const std::string& title, const std::string& msg,
const std::string& detailedText);
void populateProfilesList(const std::string& preset);
void populateWindowConfigsList(const std::string& preset);
void handleReturnFromWindowEditor(const sgct::config::Cluster& cluster,
std::filesystem::path savePath, const std::filesystem::path& saveWindowCfgPath);
void onNewWindowConfigSelection(int newIndex);
void handleReturnFromWindowEditor(std::filesystem::path savePath);
// Enables or disables the start button depending on the validity of the selected
// profile and configurations
void updateStartButton() const;
const std::filesystem::path _assetPath;
const std::filesystem::path _userAssetPath;
@@ -117,16 +128,11 @@ private:
const std::filesystem::path _profilePath;
const std::filesystem::path _userProfilePath;
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;
SplitComboBox* _profileBox = nullptr;
SplitComboBox* _windowConfigBox = nullptr;
QPushButton* _editProfileButton = nullptr;
QPushButton* _editWindowButton = nullptr;
QPushButton* _startButton = nullptr;
};
#endif // __OPENSPACE_UI_LAUNCHER___LAUNCHERWINDOW___H__

View File

@@ -52,9 +52,8 @@ public:
* \param profileBasePath The path to the folder in which all profiles live
* \param parent Pointer to parent Qt widget
*/
ProfileEdit(openspace::Profile& profile, const std::string& profileName,
ProfileEdit(openspace::Profile& profile, std::string profileName,
std::filesystem::path assetBasePath, std::filesystem::path userAssetBasePath,
std::filesystem::path builtInProfileBasePath,
std::filesystem::path profileBasePath, QWidget* parent);
/**
@@ -87,7 +86,6 @@ signals:
void raiseExitWindow();
private slots:
void duplicateProfile();
void openMeta();
void openProperties();
void openModules();
@@ -101,7 +99,7 @@ private slots:
void approved();
private:
void createWidgets(const std::string& profileName);
void createWidgets();
void initSummaryTextForEachCategory();
openspace::Profile& _profile;
@@ -111,6 +109,8 @@ private:
const std::filesystem::path _builtInProfilesPath;
bool _saveSelected = false;
std::string _profileFilename;
QLineEdit* _profileEdit = nullptr;
QLabel* _modulesLabel = nullptr;
QLabel* _assetsLabel = nullptr;

View File

@@ -27,6 +27,7 @@
#include <QWidget>
#include <sgct/config.h>
#include <vector>
class QFrame;
@@ -45,31 +46,17 @@ public:
* each monitor
* \param nMaxWindows The maximum number of windows allowed (depends on the number of
* monitors in the system)
* \param windowColors 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, bool resetToDefault,
QWidget* parent = nullptr);
DisplayWindowUnion(const std::vector<QRect>& monitorResolutions,
int nMaxWindows, QWidget* parent = nullptr);
/**
* Returns a vector of pointers to the WindowControl objects for all visible windows.
*
* \return The vector of pointers of WindowControl objects
*/
std::vector<WindowControl*> activeWindowControls() const;
void initialize(const std::vector<QRect>& monitorSizeList,
const sgct::config::Cluster& cluster);
/**
* Returns a vector of pointers to the WindowControl objects for all windows, whether
* they are visible or not.
*
* \return The vector of pointers of all WindowControl objects
*/
std::vector<WindowControl*>& windowControls();
void applyWindowSettings(std::vector<sgct::config::Window>& windows);
/**
* When called will add a new window to the set of windows, which will, in turn, send
@@ -83,14 +70,6 @@ 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 window has changed.
@@ -109,13 +88,11 @@ signals:
void nWindowsChanged(int newCount);
private:
void createWidgets(int nMaxWindows, const std::vector<QRect>& monitorResolutions,
std::array<QColor, 4> windowColors, bool resetToDefault);
void showWindows();
void updateWindows();
unsigned int _nWindowsDisplayed = 0;
std::vector<WindowControl*> _windowControl;
std::vector<WindowControl*> _windowControls;
QPushButton* _addWindowButton = nullptr;
QPushButton* _removeWindowButton = nullptr;
std::vector<QFrame*> _frameBorderLines;

View File

@@ -38,30 +38,26 @@ public:
* Constructor for MonitorBox class, which displays the system's monitor(s), their
* relative position and size, and window(s) that they contain.
*
* \param widgetDims The size of the display widget in pixels, stored in QRect
* \param monitorResolution A vector containing the monitor's maximum display size in
* pixels in a QRect object
* \param nWindows The current number of windows that has been selected by the user
* \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 widgetSize The size of the display widget in pixels
* \param monitorResolution A vector containing each monitor's maximum display size in
* pixels
* \param parent The parent which to which this MonitorBox belongs
*/
MonitorBox(QRect widgetDims, const std::vector<QRect>& monitorResolutions,
unsigned int nWindows, const std::array<QColor, 4>& windowColors,
MonitorBox(QRect widgetSize, const std::vector<QRect>& monitorResolutions,
QWidget* parent = nullptr);
public slots:
/**
* Called when window dimensions or monitor location have changed, requiring redraw.
* This will also map the window resolution into the scaled resolution of the display
* widget.
*
* \param mIdx The zero-based monitor index (primary monitor is 0)
* \param wIdx The zero-based window index
* \param newDimensions Dimensions (pixels) of window to be mapped in QRect
* \param monitorIdx The zero-based monitor index (primary monitor is 0)
* \param windowIdx The zero-based window index
* \param dimension Dimensions (pixels) of window to be mapped in QRect
*/
void windowDimensionsChanged(unsigned int mIdx, unsigned int wIdx,
const QRectF& newDimensions);
void windowDimensionsChanged(unsigned int monitorIdx, unsigned int windowIdx,
const QRectF& dimension);
/**
* Called when the number of windows that should be displayed changes.
@@ -74,21 +70,10 @@ protected:
void paintEvent(QPaintEvent* event) override;
private:
std::vector<QSizeF> computeScaledResolutionLandscape(QRectF arrangement,
const std::vector<QRect>& resolutions);
std::vector<QSizeF> computeScaledResolutionPortrait(QRectF arrangement,
const std::vector<QRect>& resolutions);
std::vector<QRectF> _monitorDimensionsScaled;
std::array<QRectF, 4> _windowRendering = {
QRectF(0.f, 0.f, 0.f, 0.f),
QRectF(0.f, 0.f, 0.f, 0.f),
QRectF(0.f, 0.f, 0.f, 0.f),
QRectF(0.f, 0.f, 0.f, 0.f)
};
int _nWindows = 1;
const std::array<QColor, 4> _colorsForWindows;
float _monitorScaleFactor = 1.0;
std::vector<QRectF> _windowRendering;
int _nWindows = 0;
float _monitorScaleFactor = 1.f;
};
#endif // __OPENSPACE_UI_LAUNCHER___MONITORBOX___H__

View File

@@ -1,148 +0,0 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_UI_LAUNCHER___SETTINGSWIDGET___H__
#define __OPENSPACE_UI_LAUNCHER___SETTINGSWIDGET___H__
#include <QComboBox>
#include <QVBoxLayout>
#include <QWidget>
#include <sgct/math.h>
class QCheckBox;
class QLabel;
class SettingsWidget final : public QWidget {
Q_OBJECT
public:
/**
* Constructor for Orientation class, which manages the overall control layout
* including monitorBox, multiple WindowControl columns, and additional controls.
*/
explicit SettingsWidget(sgct::quat orientation, QWidget* parent = nullptr);
/**
* Gets the user-provided x,y,z orientation values (degrees).
*
* \return The orientation angles provided in sgct::quat object
*/
sgct::quat orientation() const;
/**
* Gets the value for if VSync is enabled.
*
* \return `true` if the VSync option is checked/enabled
*/
bool vsync() const;
/**
* Gets whether the UI should be restricted to the first window.
*
* \return `true` if the UI should only be on the first window
*/
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 If `true` then the GUI will only be on the first window
*/
void setShowUiOnFirstWindow(bool setUiOnFirstWindow);
/**
* Sets value for whether or not the checkbox for having the UI only on the first
* window is enabled. This checkbox should only be clickable if the number of
* windows is 2 or more.
*/
void setEnableShowUiOnFirstWindowCheckbox(bool enable);
/**
* Gets the value of the selection for which display first window should mirror if
* the option to show the Ui in the first window is enabled. Note that this will
* return a value even if the checkbox is not enabled.
*
* \return `-1` if in a disabled state (e.g. when showUiOnFirstWindow() returns
* false). `0` if no window graphics are selected (only the UI will appear).
* `(1-4)` for which window's setting will be used for window 1 graphics
*/
int graphicsSelectionForShowUiOnFirstWindow() const;
/**
* Sets value of the graphics selection combo box for which other window that the
* first window will mirror.
*
* \param selection integer value for the combo box selection. `0` if no window
* graphics are selected (only the UI will appear). `(1-4)` for which window's
* setting to use for window 1 graphics
*/
void setGraphicsSelectionForShowUiOnFirstWindow(int selection);
/**
* Sets the value of the checkbox for enabling VSync.
*
* \param enableVsync If set `true` then VSync is enabled
*/
void setVsync(bool enableVsync);
/**
* Called when the number of windows that should be displayed changes.
*
* \param newCount The new number of windows included
*/
void nWindowsDisplayedChanged(int newCount);
/**
* Gets the pointer to the QComboBox that selects the graphics for first window.
*
* \return Pointer to the QComboBox object
*/
QComboBox* firstWindowGraphicsSelection();
/**
* Gets the pointer to the QCheckBox that selects if UI is in first window only.
*
* \return Pointer to the QCheckBox object
*/
QCheckBox* showUiOnFirstWindowCheckbox();
signals:
void firstWindowGraphicsSelected(int selection);
private:
void showUiOnFirstWindowClicked(bool checked);
void firstWindowGraphicsSelectionChanged(const QString& text);
sgct::quat _orientationValue = sgct::quat(0.f, 0.f, 0.f, 0.f);
QCheckBox* _checkBoxVsync = nullptr;
QCheckBox* _showUiOnFirstWindow = nullptr;
QComboBox* _firstWindowGraphicsSelection = nullptr;
QBoxLayout* _firstWindowSelectionLayout = nullptr;
int _stateOfUiOnFirstWindowPreviousCount = 1;
bool _stateOfUiOnFirstWindowWhenDisabled = false;
};
#endif // __OPENSPACE_UI_LAUNCHER___SETTINGSWIDGET___H__

View File

@@ -28,34 +28,21 @@
#include <QDialog>
#include <sgct/config.h>
#include <sgctedit/windowcontrol.h>
#include <QColor>
#include <array>
#include <filesystem>
#include <string>
class DisplayWindowUnion;
class SettingsWidget;
class QBoxLayout;
class QWidget;
class QCheckBox;
class QComboBox;
class QLineEdit;
const sgct::config::GeneratorVersion versionMin { "SgctWindowConfig", 1, 1 };
const sgct::config::GeneratorVersion versionLegacy18 { "OpenSpace", 0, 18 };
const sgct::config::GeneratorVersion versionLegacy19 { "OpenSpace", 0, 19 };
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. 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
* directory where all window configs are stored
*/
SgctEdit(QWidget* parent, std::filesystem::path userConfigPath);
/**
* Constructor for SgctEdit class, the underlying class for the full window
* configuration editor. Used when editing an existing config.
@@ -66,8 +53,8 @@ public:
* \param configBasePath The path to the folder where default config files reside
* \param parent Pointer to parent Qt widget
*/
SgctEdit(sgct::config::Cluster& cluster, std::string configName,
std::filesystem::path& configBasePath, QWidget* parent);
SgctEdit(sgct::config::Cluster cluster, std::string configName,
std::filesystem::path configBasePath, QWidget* parent);
/**
* Returns the saved filename.
@@ -76,69 +63,21 @@ public:
*/
std::filesystem::path saveFilename() const;
/**
* Returns the generated Cluster object.
*
* \return The generated Cluster object
*/
sgct::config::Cluster cluster() const;
/**
* Called when the number of windows that should be displayed changes.
*
* \param newCount The new number of windows included
*/
void nWindowsDisplayedChanged(int newCount);
/**
* Called when the checkbox for GUI only on first window is clicked.
*
* \param checked `true` if GUI is selected for first window only.
*/
void firstWindowGuiOptionClicked(bool checked);
/**
* Called when the QComboBox is selected and has a new value
*
* \param text The QString of the selected value
*/
void firstWindowGraphicsSelectionChanged(const QString& text);
private:
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 setupStateOfUiOnFirstWindow(size_t nWindows);
void deleteFromTags(sgct::config::Window& window);
void save();
void saveCluster();
void apply();
DisplayWindowUnion* _displayWidget = nullptr;
SettingsWidget* _settingsWidget = nullptr;
QCheckBox* _checkBoxVsync = nullptr;
QLineEdit* _linePitch = nullptr;
QLineEdit* _lineRoll = nullptr;
QLineEdit* _lineYaw = nullptr;
sgct::config::Cluster _cluster;
const std::filesystem::path _userConfigPath;
const std::array<QColor, 4> _colorsForWindows = {
QColor(0x2B, 0x9E, 0xC3),
QColor(0xFC, 0xAB, 0x10),
QColor(0x44, 0xAF, 0x69),
QColor(0xF8, 0x33, 0x3C)
};
std::string _configurationFilename;
QBoxLayout* _layoutButtonBox = nullptr;
QPushButton* _saveButton = nullptr;
QPushButton* _cancelButton = nullptr;
QPushButton* _applyButton = nullptr;
std::string _saveTarget;
bool _didImportValues = false;
std::string _configurationFilename;
};
#endif // __OPENSPACE_UI_LAUNCHER___SGCTEDIT___H__

View File

@@ -41,14 +41,6 @@ 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.
@@ -57,14 +49,10 @@ public:
* resides in
* \param windowIndex The zero-based window index
* \param monitorDims Vector of monitor dimensions in QRect form
* \param winColor A QColor object for this window's unique color
* \param resetToDefault If this is `true`, the widgets will be initialized to their
* default values
* \param parent The parent widget
*/
WindowControl(int monitorIndex, int windowIndex,
const std::vector<QRect>& monitorDims, const QColor& winColor,
bool resetToDefault, QWidget* parent);
const std::vector<QRect>& monitorDims, QWidget* parent);
/**
* Makes the window label at top of a window control column visible.
@@ -83,7 +71,7 @@ public:
*
* \param newDims The x, y dimensions to set the window to
*/
void setDimensions(QRectF newDims);
void setDimensions(int x, int y, int width, int height);
/**
* Sets the monitor selection combobox.
@@ -112,6 +100,9 @@ public:
*/
void setSpoutOutputState(bool shouldSpoutOutput);
void setRender2D(bool state);
void setRender3D(bool state);
/**
* Generates window configuration (sgct::config::Window struct) based on the
* GUI settings.
@@ -171,27 +162,10 @@ public:
*/
void setProjectionEquirectangular(int quality);
/**
* Controls the visibility of all projection controls, including those that are only
* shown when the projection type is set to certain values.
*
* \param enable `true` if the projections controls should be visible
*/
void setVisibilityOfProjectionGui(bool enable);
/**
* Returns an #sgct::config::Projections struct containing the projection information
* for this window.
*
* \return The object containing the projection information
*/
sgct::config::Projections generateProjectionInformation() const;
signals:
void windowChanged(int monitorIndex, int windowIndex, const QRectF& newDimensions);
private:
void createWidgets(const QColor& windowColor);
QWidget* createPlanarWidget();
QWidget* createFisheyeWidget();
QWidget* createSphericalMirrorWidget();
@@ -208,7 +182,6 @@ private:
void onFovLockClicked();
void updatePlanarLockedFov();
void setQualityComboBoxFromLinesResolution(int lines, QComboBox* combo);
static constexpr float IdealAspectRatio = 16.f / 9.f;
float _aspectRatioSize = IdealAspectRatio;
@@ -218,7 +191,7 @@ private:
bool _aspectRatioLocked = false;
bool _fovLocked = true;
std::vector<QRect> _monitorResolutions;
QRectF _windowDimensions;
QRect _windowDimensions;
QLabel* _windowNumber = nullptr;
QLineEdit* _windowName = nullptr;
@@ -228,9 +201,11 @@ private:
QSpinBox* _offsetX = nullptr;
QSpinBox* _offsetY = nullptr;
QCheckBox* _windowDecoration = nullptr;
QComboBox* _projectionType = nullptr;
QLabel* _projectionLabel = nullptr;
QCheckBox* _spoutOutput = nullptr;
QCheckBox* _render2D = nullptr;
QCheckBox* _render3D = nullptr;
QFrame* _projectionGroup = nullptr;
QComboBox* _projectionType = nullptr;
struct {
QWidget* widget = nullptr;

View File

@@ -0,0 +1,65 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_UI_LAUNCHER___SPLITCOMBOBOX___H__
#define __OPENSPACE_UI_LAUNCHER___SPLITCOMBOBOX___H__
#include <QComboBox>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
class SplitComboBox final : public QComboBox {
Q_OBJECT
public:
SplitComboBox(QWidget* parent, std::filesystem::path userPath, std::string userHeader,
std::filesystem::path hardcodedPath, std::string hardcodedHeader,
std::string specialFirst,
std::function<bool(const std::filesystem::path&)> fileFilter,
std::function<std::string(const std::filesystem::path&)> createTooltip);
void populateList(const std::string& preset);
std::pair<std::string, std::string> currentSelection() const;
signals:
// Sends the path to the selection or `std::nullopt` iff there was a special non-file
// entry at the top and that one has been selected
void selectionChanged(std::optional<std::string> selection);
private:
std::filesystem::path _userPath;
std::string _userHeader;
std::filesystem::path _hardCodedPath;
std::string _hardCodedHeader;
std::string _specialFirst;
std::function<bool(const std::filesystem::path&)> _fileFilter;
std::function<std::string(const std::filesystem::path&)> _createTooltip;
};
#endif // __OPENSPACE_UI_LAUNCHER___SPLITCOMBOBOX___H__

View File

@@ -0,0 +1,41 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_UI_LAUNCHER___WINDOWCOLORS___H__
#define __OPENSPACE_UI_LAUNCHER___WINDOWCOLORS___H__
#include <QColor>
/**
* This function returns the color that a component should use for a specific window. The
* same index will always return the same color.
*
* \param idx The index of the window for which the color should be calculated
* \return The color for the provided window index
*
* \pre \p idx must be non-negative
*/
QColor colorForWindow(int idx);
#endif // __OPENSPACE_UI_LAUNCHER___WINDOWCOLORS___H__

View File

@@ -74,6 +74,10 @@ LauncherWindow QPushButton#start {
LauncherWindow QPushButton#start:hover {
background: rgb(120, 120, 120);
}
LauncherWindow QPushButton#start:disabled {
background: rgb(175, 175, 175);
color: rgb(225, 225, 225);
}
LauncherWindow QPushButton#small {
background: rgb(86, 86, 86);

View File

@@ -0,0 +1,101 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "backgroundimage.h"
#include <ghoul/filesystem/filesystem.h>
#include <QPainter>
#include <QPixmap>
#include <random>
BackgroundImage::BackgroundImage(QRect size, const std::filesystem::path& syncFolder,
QWidget* parent)
: QLabel(parent)
{
setGeometry(size);
// Set a backup image in case no other images have been downloaded yet
setPixmap(QPixmap(":/images/launcher-background.png"));
std::filesystem::path imagePath = syncFolder / "http" / "launcher_images";
if (!std::filesystem::exists(imagePath)) {
return;
}
namespace fs = std::filesystem;
// First, we iterate through all folders in the launcher_images sync folder and we get
// the folder with the highest number
struct {
fs::directory_entry path;
int version = -1;
} latest;
for (const fs::directory_entry& p : fs::directory_iterator(imagePath)) {
if (!p.is_directory()) {
continue;
}
const std::string versionStr = p.path().stem().string();
// All folder names in the sync folder should only be a digit, so we should be
// find to just convert it here
const int version = std::stoi(versionStr);
if (version > latest.version) {
latest.version = version;
latest.path = p;
}
}
if (latest.version == -1) {
// The sync folder existed, but nothing was in there. Kinda weird, but still
return;
}
// Now we know which folder to use, we will pick an image at random
std::vector<std::filesystem::path> files = ghoul::filesystem::walkDirectory(
latest.path,
ghoul::filesystem::Recursive::No,
ghoul::filesystem::Sorted::No,
[](const std::filesystem::path& p) {
return p.extension() == ".png" && p.filename() != "overlay.png";
}
);
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(files.begin(), files.end(), g);
// There better be at least one file left, but just in in case
if (!files.empty()) {
// Take the selected image and overpaint the overlay increasing the contrast
std::string image = files.front().string();
QPixmap pixmap = QPixmap(QString::fromStdString(image));
QPainter painter = QPainter(&pixmap);
painter.setOpacity(0.7);
QPixmap overlay = QPixmap(QString::fromStdString(
std::format("{}/overlay.png", latest.path.path())
));
painter.drawPixmap(pixmap.rect(), overlay);
setPixmap(pixmap);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@
#include <openspace/scene/profile.h>
#include <ghoul/format.h>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
@@ -47,8 +48,13 @@
#include <QVBoxLayout>
#include <QWidget>
#include <filesystem>
#include <fstream>
#include <iostream>
#ifdef WIN32
#include <Windows.h>
#endif // WIN32
using namespace openspace;
namespace {
@@ -96,10 +102,9 @@ namespace {
}
} // namespace
ProfileEdit::ProfileEdit(Profile& profile, const std::string& profileName,
ProfileEdit::ProfileEdit(Profile& profile, std::string profileName,
std::filesystem::path assetBasePath,
std::filesystem::path userAssetBasePath,
std::filesystem::path builtInProfileBasePath,
std::filesystem::path profileBasePath,
QWidget* parent)
: QDialog(parent)
@@ -107,15 +112,15 @@ ProfileEdit::ProfileEdit(Profile& profile, const std::string& profileName,
, _assetBasePath(std::move(assetBasePath))
, _userAssetBasePath(std::move(userAssetBasePath))
, _profileBasePath(std::move(profileBasePath))
, _builtInProfilesPath(std::move(builtInProfileBasePath))
, _profileFilename(std::move(profileName))
{
setWindowTitle("Profile Editor");
createWidgets(profileName);
createWidgets();
initSummaryTextForEachCategory();
}
void ProfileEdit::createWidgets(const std::string& profileName) {
void ProfileEdit::createWidgets() {
QBoxLayout* layout = new QVBoxLayout(this);
QBoxLayout* topLayout = new QHBoxLayout;
QBoxLayout* leftLayout = new QVBoxLayout;
@@ -125,20 +130,12 @@ void ProfileEdit::createWidgets(const std::string& profileName) {
profileLabel->setObjectName("profile");
container->addWidget(profileLabel);
_profileEdit = new QLineEdit(QString::fromStdString(profileName));
_profileEdit->setPlaceholderText("required");
_profileEdit = new QLineEdit(QString::fromStdString(_profileFilename));
container->addWidget(_profileEdit);
QPushButton* duplicateButton = new QPushButton("Duplicate Profile");
connect(
duplicateButton, &QPushButton::clicked,
this, &ProfileEdit::duplicateProfile
);
container->addWidget(duplicateButton);
layout->addLayout(container);
}
layout->addWidget(new Line);
{
QGridLayout* container = new QGridLayout;
container->setColumnStretch(1, 1);
@@ -211,8 +208,6 @@ void ProfileEdit::createWidgets(const std::string& profileName) {
}
topLayout->addLayout(leftLayout, 3);
topLayout->addWidget(new Line);
QBoxLayout* rightLayout = new QVBoxLayout;
{
QBoxLayout* container = new QVBoxLayout;
@@ -327,6 +322,7 @@ void ProfileEdit::createWidgets(const std::string& profileName) {
container->addWidget(additionalScriptsEdit);
rightLayout->addLayout(container);
}
rightLayout->addStretch();
topLayout->addLayout(rightLayout);
layout->addLayout(topLayout);
@@ -369,49 +365,6 @@ void ProfileEdit::initSummaryTextForEachCategory() {
);
}
void ProfileEdit::duplicateProfile() {
std::string profile = _profileEdit->text().toStdString();
if (profile.empty()) {
return;
}
constexpr char Separator = '_';
int version = 0;
if (const size_t it = profile.rfind(Separator); it != std::string::npos) {
// If the value exists, we have a profile that potentially already has a version
// number attached to it
const std::string versionStr = profile.substr(it + 1);
try {
version = std::stoi(versionStr);
// We will re-add the separator with the new version string to the file, so we
// will remove the suffix here first
profile = profile.substr(0, it);
}
catch (const std::invalid_argument&) {
// If this exception is thrown, we did find a separator character but the
// substring afterwards was not a number, so the user just added a separator
// by themselves. In this case we don't do anything
}
}
// By this point we have our current profile (without any suffix) in 'profile' and the
// currently active version in 'version'. Now we need to put both together again and
// also make sure that we don't pick a version number that already exists
while (true) {
version++;
const std::string candidate = std::format("{}{}{}", profile, Separator, version);
const std::string candidatePath = std::format(
"{}{}.profile", _profileBasePath, candidate
);
if (!std::filesystem::exists(candidatePath)) {
_profileEdit->setText(QString::fromStdString(candidate));
return;
}
}
}
void ProfileEdit::openMeta() {
MetaDialog(this, &_profile.meta).exec();
@@ -475,34 +428,93 @@ bool ProfileEdit::wasSaved() const {
}
std::string ProfileEdit::specifiedFilename() const {
return _profileEdit->text().toStdString();
return _profileFilename;
}
void ProfileEdit::approved() {
std::string profileName = _profileEdit->text().toStdString();
if (profileName.empty()) {
QMessageBox::critical(this, "No profile name", "Profile name must be specified");
_profileEdit->setFocus();
// Show the save dialog
std::filesystem::path fullPath =
_profileBasePath / _profileEdit->text().toStdString();
// We might need to create intermediate directories if the user specified them in the
// name field. We do a "parent_path" here, since the last component of the path is the
// actual desired filename
std::filesystem::create_directories(std::filesystem::path(fullPath).parent_path());
const QString path = QFileDialog::getSaveFileName(
this,
"Save Profile",
QString::fromStdString(fullPath.string()),
"Profile (*.profile)",
nullptr
#ifdef __linux__
// Linux in Qt5 and Qt6 crashes when trying to access the native dialog here
, QFileDialog::DontUseNativeDialog
#endif // __linux__
);
// The user canceled the saving
if (path.isEmpty()) {
return;
}
const std::filesystem::path p = std::format(
"{}/{}.profile", _builtInProfilesPath, profileName
);
if (std::filesystem::exists(p)) {
// The filename exists in the OpenSpace-provided folder, so we don't want to allow
// a user to overwrite it
//
// If we got this far then we have a new or existing file that the user wanted to save
// the profile into
// Check if the user saved the profile in the correct folder
std::string profileBase = std::filesystem::canonical(_profileBasePath).string();
std::string file = std::filesystem::weakly_canonical(path.toStdString()).string();
if (!file.starts_with(profileBase)) {
QMessageBox::critical(
this,
"Reserved profile name",
"This is a read-only profile. Click 'Duplicate' or rename profile and save"
"File Location",
"Profiles can only be stored directly in the user profile folder or "
"subfolders inside it"
);
_profileEdit->setFocus();
return;
}
else {
_saveSelected = true;
accept();
// +1 for the directory separator
std::string filename = file.substr(profileBase.size() + 1);
try {
std::ofstream outFile;
outFile.exceptions(std::ofstream::badbit | std::ofstream::failbit);
outFile.open(path.toStdString(), std::ofstream::out);
outFile << _profile.serialize();
}
catch (const std::ofstream::failure& e) {
#ifdef WIN32
if (std::filesystem::exists(file)) {
// Check if the file is hidden, since that causes ofstream to fail
DWORD res = GetFileAttributesA(file.c_str());
if (res & FILE_ATTRIBUTE_HIDDEN) {
QMessageBox::critical(
this,
"Exception",
QString::fromStdString(std::format(
"Error writing data to file '{}' as file is marked hidden", file
))
);
return;
}
}
#endif // WIN32
QMessageBox::critical(
this,
"Exception",
QString::fromStdString(std::format(
"Error writing data to file '{}': {}", file, e.what()
))
);
return;
}
std::filesystem::path p = filename;
p.replace_extension("");
_profileFilename = p.string();
_saveSelected = true;
accept();
}
void ProfileEdit::keyPressEvent(QKeyEvent* evt) {

View File

@@ -26,6 +26,7 @@
#include "sgctedit/windowcontrol.h"
#include <ghoul/format.h>
#include <ghoul/misc/assert.h>
#include <QColor>
#include <QFrame>
#include <QPushButton>
@@ -33,45 +34,24 @@
#include <array>
#include <string>
DisplayWindowUnion::DisplayWindowUnion(const std::vector<QRect>& monitorSizeList,
int nMaxWindows,
const std::array<QColor, 4>& windowColors,
bool resetToDefault, QWidget* parent)
: QWidget(parent)
{
createWidgets(
nMaxWindows,
monitorSizeList,
windowColors,
resetToDefault
);
showWindows();
}
namespace {
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
} // namespace
void DisplayWindowUnion::createWidgets(int nMaxWindows,
const std::vector<QRect>& monitorResolutions,
std::array<QColor, 4> windowColors,
bool resetToDefault)
DisplayWindowUnion::DisplayWindowUnion(const std::vector<QRect>& monitorResolutions,
int nMaxWindows, QWidget* parent)
: QWidget(parent)
{
// Add all window controls (some will be hidden from GUI initially)
for (int i = 0; i < nMaxWindows; i++) {
const int monitorNumForThisWindow =
(monitorResolutions.size() > 1 && i >= 2) ? 1 : 0;
WindowControl* ctrl = new WindowControl(
monitorNumForThisWindow,
i,
monitorResolutions,
windowColors[i],
resetToDefault,
this
);
_windowControl.push_back(ctrl);
const int monitorIdx = (monitorResolutions.size() > 1 && i >= 2) ? 1 : 0;
WindowControl* ctrl = new WindowControl(monitorIdx, i, monitorResolutions, this);
connect(
ctrl, &WindowControl::windowChanged,
this, &DisplayWindowUnion::windowChanged
);
_windowControls.push_back(ctrl);
}
QBoxLayout* layout = new QVBoxLayout(this);
@@ -80,7 +60,7 @@ void DisplayWindowUnion::createWidgets(int nMaxWindows,
{
QBoxLayout* layoutMonButton = new QHBoxLayout;
_removeWindowButton = new QPushButton("Remove Window");
_removeWindowButton = new QPushButton("&Remove Window");
_removeWindowButton->setFocusPolicy(Qt::NoFocus);
_removeWindowButton->setToolTip(
"Remove window from the configuration (at least one window is required)"
@@ -93,7 +73,7 @@ void DisplayWindowUnion::createWidgets(int nMaxWindows,
layoutMonButton->addStretch(1);
_addWindowButton = new QPushButton("Add Window");
_addWindowButton = new QPushButton("&Add Window");
_addWindowButton->setToolTip(QString::fromStdString(std::format(
"Add a window to the configuration (up to {} windows allowed)", nMaxWindows
)));
@@ -106,70 +86,172 @@ void DisplayWindowUnion::createWidgets(int nMaxWindows,
layout->addLayout(layoutMonButton);
}
QFrame* line = new QFrame;
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
layout->addWidget(line);
{
QFrame* line = new QFrame;
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
layout->addWidget(line);
}
QBoxLayout* layoutWindows = new QHBoxLayout;
layoutWindows->setContentsMargins(0, 0, 0, 0);
layoutWindows->setSpacing(0);
for (int i = 0; i < nMaxWindows; i++) {
layoutWindows->addWidget(_windowControl[i]);
if (i < (nMaxWindows - 1)) {
QFrame* frameForNextWindow = new QFrame;
frameForNextWindow->setFrameShape(QFrame::VLine);
_frameBorderLines.push_back(frameForNextWindow);
layoutWindows->addWidget(frameForNextWindow);
{
QBoxLayout* layoutWindows = new QHBoxLayout;
layoutWindows->setContentsMargins(0, 0, 0, 0);
layoutWindows->setSpacing(0);
for (int i = 0; i < nMaxWindows; i++) {
layoutWindows->addWidget(_windowControls[i]);
if (i < (nMaxWindows - 1)) {
QFrame* frameForNextWindow = new QFrame;
frameForNextWindow->setFrameShape(QFrame::VLine);
_frameBorderLines.push_back(frameForNextWindow);
layoutWindows->addWidget(frameForNextWindow);
}
}
layout->addLayout(layoutWindows);
}
layout->addLayout(layoutWindows);
layout->addStretch();
}
std::vector<WindowControl*> DisplayWindowUnion::activeWindowControls() const {
std::vector<WindowControl*> res;
res.reserve(_nWindowsDisplayed);
for (unsigned int i = 0; i < _nWindowsDisplayed; i++) {
res.push_back(_windowControl[i]);
void DisplayWindowUnion::initialize(const std::vector<QRect>& monitorSizeList,
const sgct::config::Cluster& cluster)
{
for (int i = 0; i < cluster.nodes.front().windows.size(); i++) {
addWindow();
}
const size_t nWindows = std::min(
cluster.nodes.front().windows.size(),
_windowControls.size()
);
for (size_t i = 0; i < nWindows; i++) {
const sgct::config::Window& w = cluster.nodes.front().windows[i];
WindowControl* wCtrl = _windowControls[i];
ghoul_assert(wCtrl, "No window control");
//
// Get monitor index for the window
uint8_t monitorNum = 0;
if (w.monitor.has_value()) {
monitorNum = *w.monitor;
if (monitorNum > (monitorSizeList.size() - 1)) {
monitorNum = 0;
}
}
//
// Get position for the window in monitor coordinates
unsigned int posX = 0;
unsigned int posY = 0;
wCtrl->setMonitorSelection(monitorNum);
if (w.pos.has_value()) {
posX = w.pos->x;
posY = w.pos->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 (monitorSizeList.size() > monitorNum) {
posX -= monitorSizeList[monitorNum].x();
posY -= monitorSizeList[monitorNum].y();
}
}
wCtrl->setDimensions(posX, posY, w.size.x, w.size.y);
//
// Get Window name
if (w.name.has_value()) {
wCtrl->setWindowName(*w.name);
}
//
// Get decoration state
if (w.isDecorated.has_value()) {
wCtrl->setDecorationState(*w.isDecorated);
}
//
// Get Spout state
wCtrl->setSpoutOutputState(w.spout.has_value() && w.spout->enabled);
//
// Get render states
if (w.draw2D.has_value()) {
wCtrl->setRender2D(*w.draw2D);
}
if (w.draw3D.has_value()) {
wCtrl->setRender3D(*w.draw3D);
}
//
// Get projection-based settings depending on the projection in the window
std::visit(overloaded{
[&](const sgct::config::CylindricalProjection& p) {
if (p.quality.has_value() && p.heightOffset.has_value()) {
wCtrl->setProjectionCylindrical(*p.quality, *p.heightOffset);
}
},
[&](const sgct::config::EquirectangularProjection& p) {
if (p.quality.has_value()) {
wCtrl->setProjectionEquirectangular(*p.quality);
}
},
[&](const sgct::config::FisheyeProjection& p) {
if (p.quality.has_value()) {
wCtrl->setProjectionFisheye(*p.quality);
}
},
[&](const 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)
);
},
[&](const sgct::config::SphericalMirrorProjection& p) {
if (p.quality.has_value()) {
wCtrl->setProjectionSphericalMirror(*p.quality);
}
},
[&](const sgct::config::NoProjection&) {},
[&](const sgct::config::ProjectionPlane&) {},
[&](const sgct::config::CubemapProjection&) {},
},
w.viewports.back().projection
);
}
return res;
}
std::vector<WindowControl*>& DisplayWindowUnion::windowControls() {
return _windowControl;
void DisplayWindowUnion::applyWindowSettings(std::vector<sgct::config::Window>& windows) {
windows.resize(_nWindowsDisplayed);
for (size_t wIdx = 0; wIdx < _nWindowsDisplayed; wIdx++) {
ghoul_assert(_windowControls[wIdx], "No window control");
_windowControls[wIdx]->generateWindowInformation(windows[wIdx]);
}
}
void DisplayWindowUnion::addWindow() {
if (_nWindowsDisplayed < _windowControl.size()) {
_windowControl[_nWindowsDisplayed]->resetToDefaults();
if (_nWindowsDisplayed < _windowControls.size()) {
_windowControls[_nWindowsDisplayed]->resetToDefaults();
_nWindowsDisplayed++;
showWindows();
updateWindows();
}
}
void DisplayWindowUnion::removeWindow() {
if (_nWindowsDisplayed > 1) {
_nWindowsDisplayed--;
showWindows();
updateWindows();
}
}
unsigned int DisplayWindowUnion::numWindowsDisplayed() const {
return _nWindowsDisplayed;
}
void DisplayWindowUnion::showWindows() {
for (size_t i = 0; i < _windowControl.size(); i++) {
_windowControl[i]->setVisible(i < _nWindowsDisplayed);
void DisplayWindowUnion::updateWindows() {
for (size_t i = 0; i < _windowControls.size(); i++) {
_windowControls[i]->setVisible(i < _nWindowsDisplayed);
}
for (size_t i = 0; i < _frameBorderLines.size(); i++) {
_frameBorderLines[i]->setVisible(i < (_nWindowsDisplayed - 1));
}
_removeWindowButton->setEnabled(_nWindowsDisplayed > 1);
_addWindowButton->setEnabled(_nWindowsDisplayed != _windowControl.size());
for (WindowControl* w : _windowControl) {
_addWindowButton->setEnabled(_nWindowsDisplayed != _windowControls.size());
for (WindowControl* w : _windowControls) {
w->showWindowLabel(_nWindowsDisplayed > 1);
}
emit nWindowsChanged(_nWindowsDisplayed);
}

View File

@@ -24,178 +24,70 @@
#include "sgctedit/monitorbox.h"
#include <ghoul/misc/assert.h>
#include "windowcolors.h"
#include <QPainter>
namespace {
constexpr float MarginFractionWidgetSize = 0.05f;
constexpr int WindowOpacity = 170;
QRectF computeUnion(const std::vector<QRect>& monitorResolutions) {
QRectF res = QRectF(0.f, 0.f, 0.f, 0.f);
for (const QRect& m : monitorResolutions) {
res |= m;
}
return res;
}
} // namespace
MonitorBox::MonitorBox(QRect widgetDims, const std::vector<QRect>& monitorResolutions,
unsigned int nWindows, const std::array<QColor, 4>& windowColors,
MonitorBox::MonitorBox(QRect widgetSize, const std::vector<QRect>& monitorResolutions,
QWidget* parent)
: QWidget(parent)
, _nWindows(nWindows)
, _colorsForWindows(windowColors)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
const QRectF monitorArrangement = computeUnion(monitorResolutions);
constexpr float MarginFractionWidgetSize = 0.05f;
//
// Calculate the collective size of the monitors
QRectF monitorArrangement = QRectF(0.f, 0.f, 0.f, 0.f);
for (const QRect& m : monitorResolutions) {
monitorArrangement |= m;
}
//
// Set the size of the widget according to the aspect ratio of the total size
const float aspectRatio = monitorArrangement.width() / monitorArrangement.height();
if (aspectRatio > 1.0) {
const float borderMargin = 2.f * MarginFractionWidgetSize * widgetDims.width();
widgetDims.setHeight(widgetDims.width() / aspectRatio + borderMargin);
if (aspectRatio > 1.f) {
const float borderMargin = 2.f * MarginFractionWidgetSize * widgetSize.width();
widgetSize.setHeight(widgetSize.width() / aspectRatio + borderMargin);
}
else {
const float borderMargin = 2.f * MarginFractionWidgetSize * widgetDims.height();
widgetDims.setWidth(widgetDims.height() * aspectRatio + borderMargin);
const float borderMargin = 2.f * MarginFractionWidgetSize * widgetSize.height();
widgetSize.setWidth(widgetSize.height() * aspectRatio + borderMargin);
}
setFixedSize(widgetDims.width(), widgetDims.height());
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFixedSize(widgetSize.width(), widgetSize.height());
//
// Map monitor resolution to widget coordinates
std::vector<QSizeF> offsets =
aspectRatio >= 1.f ?
computeScaledResolutionLandscape(monitorArrangement, monitorResolutions) :
computeScaledResolutionPortrait(monitorArrangement, monitorResolutions);
for (size_t i = 0; i < monitorResolutions.size(); i++) {
_monitorDimensionsScaled.emplace_back(
offsets[i].width(),
offsets[i].height(),
monitorResolutions[i].width() * _monitorScaleFactor,
monitorResolutions[i].height() * _monitorScaleFactor
);
}
}
void MonitorBox::paintEvent(QPaintEvent*) {
QPainter painter(this);
//
// Draw widget border
constexpr int Radius = 10;
painter.setPen(QPen(Qt::gray, 4));
painter.drawRoundedRect(0, 0, width() - 1, height() - 1, Radius, Radius);
//
// Draw window out-of-bounds region(s) first
for (int i = 0; i < _nWindows; i++) {
painter.setBrush(Qt::BDiagPattern);
painter.setPen(QPen(_colorsForWindows[i], 0));
painter.drawRect(_windowRendering[i]);
}
// Draw & fill monitors over the out-of-bounds regions
painter.setPen(QPen(Qt::black, 2));
painter.setBrush(Qt::NoBrush);
for (size_t i = 0; i < _monitorDimensionsScaled.size(); i++) {
const QColor Grey = QColor(0xDD, 0xDD, 0xDD);
painter.drawRect(_monitorDimensionsScaled[i]);
painter.fillRect(_monitorDimensionsScaled[i], QBrush(Grey, Qt::SolidPattern));
if (_monitorDimensionsScaled.size() > 1 && i == 0) {
// We only want to render the "Primary" if there are multiple windows
const QPointF textPos = QPointF(
_monitorDimensionsScaled[i].left() + 4.0,
_monitorDimensionsScaled[i].top() + 24.0
);
QFont f("Arial");
f.setPixelSize(24);
painter.setFont(f);
painter.drawText(textPos, "Primary");
}
}
// Draw window number(s) first for darker contrast, then window(s) over both
// out-of-bounds and monitors
for (int i = 0; i < _nWindows; i++) {
QPointF p = QPointF(
_windowRendering[i].left() + 5.0,
_windowRendering[i].bottom() - 5.0
);
p.setX(std::clamp(p.x(), 0.0, static_cast<double>(size().width()) - 10.0));
p.setY(std::clamp(p.y(), 20.0, static_cast<double>(size().height())));
painter.drawText(p, QString::number(i + 1));
}
//
// Paint window
for (int i = 0; i < _nWindows; i++) {
painter.setPen(QPen(_colorsForWindows[i], 1));
painter.drawRect(_windowRendering[i]);
QColor fillColor = _colorsForWindows[i];
fillColor.setAlpha(WindowOpacity);
painter.fillRect(_windowRendering[i], QBrush(fillColor, Qt::SolidPattern));
}
}
void MonitorBox::windowDimensionsChanged(unsigned int mIdx, unsigned int wIdx,
const QRectF& newDimensions)
{
_windowRendering[wIdx] = QRectF(
_monitorDimensionsScaled[mIdx].x() + newDimensions.left() * _monitorScaleFactor,
_monitorDimensionsScaled[mIdx].y() + newDimensions.top() * _monitorScaleFactor,
newDimensions.width() * _monitorScaleFactor,
newDimensions.height() * _monitorScaleFactor
);
update();
}
std::vector<QSizeF> MonitorBox::computeScaledResolutionLandscape(QRectF arrangement,
const std::vector<QRect>& resolutions)
{
std::vector<QSizeF> offsets;
const float margin = size().width() * MarginFractionWidgetSize;
const float virtualWidth = size().width() * (1.f - MarginFractionWidgetSize * 2.f);
_monitorScaleFactor = virtualWidth / arrangement.width();
_monitorScaleFactor = virtualWidth / monitorArrangement.width();
const float aspectRatio = arrangement.width() / arrangement.height();
const float newHeight = virtualWidth / aspectRatio;
for (const QRect& res : resolutions) {
const float x = margin + (res.x() - arrangement.x()) * _monitorScaleFactor;
for (const QRect& res : monitorResolutions) {
const float x = margin + (res.x() - monitorArrangement.x()) * _monitorScaleFactor;
const float y = margin + (size().height() - newHeight - margin) / 4.f +
(res.y() - arrangement.y()) * _monitorScaleFactor;
offsets.emplace_back(x, y);
(res.y() - monitorArrangement.y()) * _monitorScaleFactor;
const float width = res.width() * _monitorScaleFactor;
const float height = res.height() * _monitorScaleFactor;
_monitorDimensionsScaled.emplace_back(x, y, width, height);
}
return offsets;
}
std::vector<QSizeF> MonitorBox::computeScaledResolutionPortrait(QRectF arrangement,
const std::vector<QRect>& resolutions)
void MonitorBox::windowDimensionsChanged(unsigned int monitorIdx, unsigned int windowIdx,
const QRectF& dimension)
{
std::vector<QSizeF> offsets;
const float marginWidget = size().height() * MarginFractionWidgetSize;
const float virtualHeight = size().height() * (1.f - MarginFractionWidgetSize * 2.f);
_monitorScaleFactor = virtualHeight / arrangement.height();
const float aspectRatio = arrangement.width() / arrangement.height();
const float newWidth = virtualHeight * aspectRatio;
for (const QRect& res : resolutions) {
const float x = marginWidget + (size().width() - newWidth - marginWidget) / 4.f +
(res.x() - arrangement.x()) * _monitorScaleFactor;
const float y = marginWidget + (res.y() - arrangement.y()) * _monitorScaleFactor;
offsets.emplace_back(x, y);
if (windowIdx >= _windowRendering.size()) {
// We were told about a window change for a window we haven't created yet, so
// initialize the vector so that we have enough windows available
_windowRendering.resize(windowIdx + 1, QRectF(0.f, 0.f, 0.f, 0.f));
}
return offsets;
_windowRendering[windowIdx] = QRectF(
_monitorDimensionsScaled[monitorIdx].x() + dimension.left() * _monitorScaleFactor,
_monitorDimensionsScaled[monitorIdx].y() + dimension.top() * _monitorScaleFactor,
dimension.width() * _monitorScaleFactor,
dimension.height() * _monitorScaleFactor
);
update();
}
void MonitorBox::nWindowsDisplayedChanged(int nWindows) {
@@ -204,3 +96,72 @@ void MonitorBox::nWindowsDisplayedChanged(int nWindows) {
update();
}
}
void MonitorBox::paintEvent(QPaintEvent*) {
QPainter painter(this);
//
// Draw widget border
constexpr double RectRadius = 10.0;
painter.setPen(QPen(Qt::gray, 4));
painter.drawRoundedRect(0, 0, width() - 1, height() - 1, RectRadius, RectRadius);
//
// Draw window out-of-bounds region(s) first
for (int i = 0; i < _nWindows; i++) {
painter.setBrush(Qt::BDiagPattern);
painter.setPen(QPen(colorForWindow(i), 0));
painter.drawRect(_windowRendering[i]);
}
//
// Draw & fill monitors over the out-of-bounds regions
painter.setPen(QPen(Qt::black, 2));
painter.setBrush(Qt::NoBrush);
for (size_t i = 0; i < _monitorDimensionsScaled.size(); i++) {
constexpr QColor Grey = QColor(221, 221, 221);
painter.drawRect(_monitorDimensionsScaled[i]);
painter.fillRect(_monitorDimensionsScaled[i], QBrush(Grey, Qt::SolidPattern));
// We only want to render the "Primary" if there are multiple windows
if (_monitorDimensionsScaled.size() > 1 && i == 0) {
const QPointF textPos = QPointF(
_monitorDimensionsScaled[i].left() + 4.0,
_monitorDimensionsScaled[i].top() + 24.0
);
QFont f = QFont("Arial");
f.setPixelSize(24);
painter.setFont(f);
painter.drawText(textPos, "Primary");
}
}
//
// Draw window number(s) first for darker contrast, then window(s) over both
// out-of-bounds and monitors
for (int i = 0; i < _nWindows; i++) {
const double x = _windowRendering[i].left() + 5.0;
const double y = _windowRendering[i].bottom() - 5.0;
const QPointF p = QPointF(
std::clamp(x, 0.0, static_cast<double>(size().width()) - 10.0),
std::clamp(y, 20.0, static_cast<double>(size().height()))
);
painter.drawText(p, QString::number(i + 1));
}
//
// Paint window
for (int i = 0; i < _nWindows; i++) {
constexpr int WindowOpacity = 170;
QColor color = colorForWindow(i);
painter.setPen(QPen(color, 1));
painter.drawRect(_windowRendering[i]);
QColor fillColor = color;
fillColor.setAlpha(WindowOpacity);
painter.fillRect(_windowRendering[i], QBrush(fillColor, Qt::SolidPattern));
}
}

View File

@@ -1,110 +0,0 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "sgctedit/orientationdialog.h"
#include <QDialogButtonBox>
#include <QDoubleValidator>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <ghoul/glm.h>
OrientationDialog::OrientationDialog(sgct::quat& orientation, QWidget* parent)
: QDialog(parent)
, _orientationValue(orientation)
{
setWindowTitle("Global Orientation");
QGridLayout* layoutWindow = new QGridLayout(this);
{
const QString pitchTip = "Pitch or elevation: negative numbers tilt the camera "
"downwards; positive numbers tilt upwards.\nThe allowed range is [-90, 90]. "
"Internally, this corresponds to the x value in the quaternion";
QLabel* labelPitch = new QLabel("Pitch");
labelPitch->setToolTip(pitchTip);
layoutWindow->addWidget(labelPitch, 0, 0);
_linePitch = new QLineEdit;
_linePitch->setText(QString::number(glm::degrees(_orientationValue.x)));
_linePitch->setToolTip(pitchTip);
QDoubleValidator* validatorPitch = new QDoubleValidator(-90.0, 90.0, 15);
validatorPitch->setNotation(QDoubleValidator::StandardNotation);
_linePitch->setValidator(validatorPitch);
layoutWindow->addWidget(_linePitch, 0, 1);
}
{
const QString rollTip = "Roll or bank: negative numbers rotate the camera "
"counter-clockwise; positive numbers clockwise.\nThe allowed range is "
"[-180, 180]. Internally, this corresponds to the z value in the quaternion";
QLabel* labelRoll = new QLabel("Roll");
labelRoll->setToolTip(rollTip);
layoutWindow->addWidget(labelRoll, 1, 0);
_lineRoll = new QLineEdit;
_lineRoll->setText(QString::number(glm::degrees(_orientationValue.z)));
_lineRoll->setToolTip(rollTip);
QDoubleValidator* validatorRoll = new QDoubleValidator(-360.0, 360.0, 15);
validatorRoll->setNotation(QDoubleValidator::StandardNotation);
_lineRoll->setValidator(validatorRoll);
layoutWindow->addWidget(_lineRoll, 1, 1);
}
{
const QString yawTip = "Yaw, heading, or azimuth: negative numbers pan the "
"camera to the left; positive numbers pan to the\nright. The allowed range "
"is [-360, 360]. Internally, this corresponds to the y value in the "
"quaternion";
QLabel* labelYaw = new QLabel;
labelYaw ->setText("Yaw");
labelYaw->setToolTip(yawTip);
layoutWindow->addWidget(labelYaw, 2, 0);
_lineYaw = new QLineEdit;
_lineYaw->setText(QString::number(glm::degrees(_orientationValue.y)));
_lineYaw->setToolTip(yawTip);
QDoubleValidator* validatorYaw = new QDoubleValidator(-180.0, 180.0, 15, this);
validatorYaw->setNotation(QDoubleValidator::StandardNotation);
_lineYaw->setValidator(validatorYaw);
layoutWindow->addWidget(_lineYaw, 2, 1);
}
{
QDialogButtonBox* buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel
);
connect(buttons, &QDialogButtonBox::accepted, this, &OrientationDialog::ok);
connect(buttons, &QDialogButtonBox::rejected, this, &OrientationDialog::reject);
layoutWindow->addWidget(buttons, 3, 0, 1, 2);
}
}
void OrientationDialog::ok() {
_orientationValue.x = glm::radians(_linePitch->text().toFloat());
_orientationValue.y = glm::radians(_lineYaw->text().toFloat());
_orientationValue.z = glm::radians(_lineRoll->text().toFloat());
_orientationValue.w = 1.0;
accept();
}

View File

@@ -1,169 +0,0 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "sgctedit/settingswidget.h"
#include "sgctedit/orientationdialog.h"
#include <QCheckBox>
#include <QPushButton>
#include <QVBoxLayout>
SettingsWidget::SettingsWidget(sgct::quat orientation, QWidget* parent)
: QWidget(parent)
, _orientationValue(std::move(orientation))
, _showUiOnFirstWindow(new QCheckBox(
"Show user interface only on first window using graphics:"
))
, _firstWindowGraphicsSelection(new QComboBox)
, _firstWindowSelectionLayout(new QHBoxLayout)
{
QBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
_showUiOnFirstWindow->setChecked(false);
_showUiOnFirstWindow->setEnabled(false);
_showUiOnFirstWindow->setToolTip(
"If enabled the first window is marked as a GUI window resulting in the user "
"interface only being shown\non that window and the rendering is suppressed on "
"this first window. The remaining windows will render\nnormally but they will "
"not show the user interface"
);
_firstWindowGraphicsSelection->setToolTip(
"Select the contents of the first window to match one of the other windows"
);
_firstWindowGraphicsSelection->setFixedWidth(150);
connect(
_showUiOnFirstWindow, &QCheckBox::clicked,
this, &SettingsWidget::showUiOnFirstWindowClicked
);
_firstWindowSelectionLayout->addWidget(_showUiOnFirstWindow);
_firstWindowSelectionLayout->addWidget(_firstWindowGraphicsSelection);
_firstWindowSelectionLayout->addStretch();
layout->addLayout(_firstWindowSelectionLayout);
_checkBoxVsync = new QCheckBox("Enable VSync");
_checkBoxVsync->setToolTip(
"If enabled the framerate will be locked to the refresh rate of the monitor"
);
layout->addWidget(_checkBoxVsync);
QPushButton* orientationButton = new QPushButton("Global Orientation");
orientationButton->setToolTip(
"Opens a separate dialog for setting the pitch, yaw, and roll of the camera\n"
"(the orientation applies to all viewports)"
);
orientationButton->setFocusPolicy(Qt::NoFocus);
layout->addWidget(orientationButton);
connect(
orientationButton, &QPushButton::released,
[this]() {
OrientationDialog _orientationDialog(_orientationValue, this);
_orientationDialog.exec();
}
);
}
sgct::quat SettingsWidget::orientation() const {
return _orientationValue;
}
bool SettingsWidget::vsync() const {
return _checkBoxVsync->isChecked();
}
bool SettingsWidget::showUiOnFirstWindow() const {
return (_showUiOnFirstWindow->isChecked() && _showUiOnFirstWindow->isEnabled());
}
void SettingsWidget::setShowUiOnFirstWindow(bool setUiOnFirstWindow) {
_showUiOnFirstWindow->setChecked(setUiOnFirstWindow);
}
void SettingsWidget::setEnableShowUiOnFirstWindowCheckbox(bool enable) {
_showUiOnFirstWindow->setEnabled(enable);
_firstWindowGraphicsSelection->setEnabled(enable);
}
int SettingsWidget::graphicsSelectionForShowUiOnFirstWindow() const {
return _firstWindowGraphicsSelection->currentIndex();
}
void SettingsWidget::setGraphicsSelectionForShowUiOnFirstWindow(int selection) {
_firstWindowGraphicsSelection->setCurrentIndex(selection);
}
void SettingsWidget::setVsync(bool enableVsync) {
_checkBoxVsync->setChecked(enableVsync);
}
void SettingsWidget::nWindowsDisplayedChanged(int newCount) {
constexpr int CountOneWindow = 1;
constexpr int CountTwoWindows = 2;
int graphicsSelect = _firstWindowGraphicsSelection->currentIndex();
graphicsSelect = std::max(0, graphicsSelect);
QList<QString> graphicsOptions = {"None (GUI only)"};
for (int i = CountOneWindow; i <= newCount; i++) {
graphicsOptions.append("Window " + QString::number(i));
}
_firstWindowGraphicsSelection->clear();
_firstWindowGraphicsSelection->addItems(graphicsOptions);
setEnableShowUiOnFirstWindowCheckbox(newCount > CountOneWindow);
if (graphicsSelect > newCount) {
graphicsSelect = newCount;
}
_firstWindowGraphicsSelection->setCurrentIndex(graphicsSelect);
if (newCount == CountOneWindow) {
_stateOfUiOnFirstWindowWhenDisabled = _showUiOnFirstWindow->isChecked();
_showUiOnFirstWindow->setChecked(false);
_firstWindowGraphicsSelection->setEnabled(false);
}
else if (newCount == CountTwoWindows &&
_stateOfUiOnFirstWindowPreviousCount == CountOneWindow)
{
if (_stateOfUiOnFirstWindowWhenDisabled) {
_showUiOnFirstWindow->setChecked(true);
}
_firstWindowGraphicsSelection->setEnabled(_showUiOnFirstWindow->isChecked());
}
else {
_firstWindowGraphicsSelection->setEnabled(_showUiOnFirstWindow->isChecked());
}
_stateOfUiOnFirstWindowPreviousCount = newCount;
}
void SettingsWidget::showUiOnFirstWindowClicked(bool checked) {
_firstWindowGraphicsSelection->setEnabled(checked);
}
QComboBox* SettingsWidget::firstWindowGraphicsSelection() {
return _firstWindowGraphicsSelection;
}
QCheckBox* SettingsWidget::showUiOnFirstWindowCheckbox() {
return _showUiOnFirstWindow;
}

View File

@@ -26,20 +26,22 @@
#include <sgctedit/displaywindowunion.h>
#include <sgctedit/monitorbox.h>
#include <sgctedit/settingswidget.h>
#include <sgctedit/windowcontrol.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/misc/assert.h>
#include <QApplication>
#include <QCheckBox>
#include <QColor>
#include <QComboBox>
#include <QFileDialog>
#include <QFrame>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QScreen>
#include <QVBoxLayout>
#include <filesystem>
#include <fstream>
namespace {
constexpr QRect MonitorWidgetSize = QRect(0, 0, 500, 500);
constexpr int MaxNumberWindows = 4;
// Returns true if the windows are not ordered correctly. 'Correct' in this means that
@@ -66,218 +68,50 @@ namespace {
return false;
}
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::vector<QRect> createMonitorInfoSet() {
std::vector<QRect> monitorSizes;
for (QScreen* screen : qApp->screens()) {
const QSize size = screen->size();
const QRect geometry = screen->availableGeometry();
const int actualWidth = std::max(size.width(), geometry.width());
const int actualHeight = std::max(size.height(), geometry.height());
monitorSizes.emplace_back(
geometry.x(),
geometry.y(),
static_cast<int>(actualWidth * screen->devicePixelRatio()),
static_cast<int>(actualHeight * screen->devicePixelRatio())
);
}
return monitorSizes;
}
} // namespace
SgctEdit::SgctEdit(QWidget* parent, std::filesystem::path userConfigPath)
SgctEdit::SgctEdit(sgct::config::Cluster cluster, std::string configName,
std::filesystem::path configBasePath, QWidget* parent)
: QDialog(parent)
, _userConfigPath(std::move(userConfigPath))
{
setWindowTitle("Window Configuration Editor");
createWidgets(createMonitorInfoSet(), 1, true);
}
SgctEdit::SgctEdit(sgct::config::Cluster& cluster, std::string configName,
std::filesystem::path& configBasePath, QWidget* parent)
: QDialog(parent)
, _cluster(cluster)
, _userConfigPath(configBasePath)
, _cluster(std::move(cluster))
, _userConfigPath(std::move(configBasePath))
, _configurationFilename(std::move(configName))
, _didImportValues(true)
{
setWindowTitle("Window Configuration Editor");
const size_t nWindows = _cluster.nodes.front().windows.size();
std::vector<QRect> monitorSizes = createMonitorInfoSet();
createWidgets(monitorSizes, static_cast<unsigned int>(nWindows), false);
const 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();
}
}
const QRectF newDims = QRectF(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());
wCtrl->setSpoutOutputState(w.spout.has_value() && w.spout->enabled);
}
setupProjectionTypeInGui(w.viewports.back(), wCtrl);
}
setupStateOfUiOnFirstWindow(nWindows);
_settingsWidget->setVsync(
_cluster.settings &&
_cluster.settings.value().display &&
_cluster.settings.value().display.value().swapInterval
);
}
void SgctEdit::setupStateOfUiOnFirstWindow(size_t nWindows) {
bool firstWindowGuiIsEnabled = (nWindows > 1);
int graphicsSelectionForFirstWindow = 0;
int nGuiRenderTagsFound = 0;
_settingsWidget->nWindowsDisplayedChanged(static_cast<int>(nWindows));
for (size_t i = 0; i < nWindows; i++) {
sgct::config::Window& w = _cluster.nodes.front().windows[i];
//First window needs to have "GUI" tag if this mode is set
if (i == 0) {
firstWindowGuiIsEnabled =
(std::find(w.tags.begin(), w.tags.end(), "GUI") != w.tags.end());
if (std::find(w.tags.begin(), w.tags.end(), "GUI_No_Render") != w.tags.end())
{
graphicsSelectionForFirstWindow = 0;
nGuiRenderTagsFound++;
}
for (int winNum = 0; winNum <= 4; ++winNum) {
const std::string searchTag = "GUI_Render_Win" + std::to_string(winNum);
if (std::find(w.tags.begin(), w.tags.end(), searchTag) != w.tags.end()) {
graphicsSelectionForFirstWindow = winNum;
nGuiRenderTagsFound++;
}
}
}
// If first window only renders 2D, and all subsequent windows do not, then
// will enable the checkbox option for showing GUI only on first window
if (w.draw2D.has_value()) {
firstWindowGuiIsEnabled &= (i == 0) ? w.draw2D.value() : !w.draw2D.value();
}
else {
firstWindowGuiIsEnabled = false;
}
}
if ((nGuiRenderTagsFound > 1) ||
(graphicsSelectionForFirstWindow > static_cast<int>(nWindows)))
{
graphicsSelectionForFirstWindow = 0;
}
_settingsWidget->setShowUiOnFirstWindow(firstWindowGuiIsEnabled);
if (firstWindowGuiIsEnabled) {
// Call these again in order to ensure that GUI is configured correctly based on
// the values read from the config file
_settingsWidget->setEnableShowUiOnFirstWindowCheckbox(true);
_settingsWidget->nWindowsDisplayedChanged(static_cast<int>(nWindows));
}
_settingsWidget->setGraphicsSelectionForShowUiOnFirstWindow(
graphicsSelectionForFirstWindow
);
}
void SgctEdit::setupProjectionTypeInGui(sgct::config::Viewport& vPort,
WindowControl* wCtrl)
{
std::visit(overloaded {
[&](const sgct::config::CylindricalProjection& p) {
if (p.quality && p.heightOffset) {
wCtrl->setProjectionCylindrical(
*p.quality,
*p.heightOffset
);
}
},
[&](const sgct::config::EquirectangularProjection& p) {
if (p.quality) {
wCtrl->setProjectionEquirectangular(*p.quality);
}
},
[&](const sgct::config::FisheyeProjection& p) {
if (p.quality) {
wCtrl->setProjectionFisheye(*p.quality);
}
},
[&](const 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))
);
},
[&](const sgct::config::SphericalMirrorProjection& p) {
if (p.quality) {
wCtrl->setProjectionSphericalMirror(
*p.quality
);
}
},
[&](const sgct::config::NoProjection&) {},
[&](const sgct::config::ProjectionPlane&) {},
[&](const sgct::config::CubemapProjection&) {},
}, vPort.projection);
}
std::vector<QRect> SgctEdit::createMonitorInfoSet() {
const QList<QScreen*> screens = qApp->screens();
const int nScreensManaged = std::min(static_cast<int>(screens.length()), 4);
std::vector<QRect> monitorSizes;
for (int s = 0; s < nScreensManaged; ++s) {
const QSize size = screens[s]->size();
const QRect geometry = screens[s]->availableGeometry();
const int actualWidth = std::max(size.width(), geometry.width());
const int actualHeight = std::max(size.height(), geometry.height());
monitorSizes.emplace_back(
geometry.x(),
geometry.y(),
static_cast<int>(actualWidth * screens[s]->devicePixelRatio()),
static_cast<int>(actualHeight * screens[s]->devicePixelRatio())
);
}
return monitorSizes;
}
void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes,
unsigned int nWindows, bool setToDefaults)
{
QBoxLayout* layout = new QVBoxLayout(this);
layout->setSizeConstraint(QLayout::SetFixedSize);
sgct::quat orientation = sgct::quat(0.f, 0.f, 0.f, 0.f);
if (_cluster.scene.has_value() && _cluster.scene->orientation.has_value()) {
orientation = *_cluster.scene->orientation;
}
//
// Monitor widget at the top of the window
{
MonitorBox* monitorBox = new MonitorBox(
MonitorWidgetSize,
monitorSizes,
MaxNumberWindows,
_colorsForWindows,
this
);
constexpr QRect MonitorWidgetSize = QRect(0, 0, 500, 500);
MonitorBox* monitorBox = new MonitorBox(MonitorWidgetSize, monitorSizes);
layout->addWidget(monitorBox, 0, Qt::AlignCenter);
QFrame* displayFrame = new QFrame;
displayFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
QBoxLayout* displayLayout = new QVBoxLayout(displayFrame);
_displayWidget = new DisplayWindowUnion(
monitorSizes,
MaxNumberWindows,
_colorsForWindows,
setToDefaults,
this
);
_displayWidget = new DisplayWindowUnion(monitorSizes, MaxNumberWindows, this);
connect(
_displayWidget, &DisplayWindowUnion::windowChanged,
monitorBox, &MonitorBox::windowDimensionsChanged
@@ -286,45 +120,112 @@ void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes,
_displayWidget, &DisplayWindowUnion::nWindowsChanged,
monitorBox, &MonitorBox::nWindowsDisplayedChanged
);
// We initialize the widget after making the connections so that the monitorbox
// widget will get informed about the windows and their sizes automatically
_displayWidget->initialize(monitorSizes, _cluster);
layout->addWidget(_displayWidget);
}
for (unsigned int i = 0; i < nWindows; i++) {
_displayWidget->addWindow();
}
displayLayout->addWidget(_displayWidget);
layout->addWidget(displayFrame);
QWidget* settingsContainer = new QWidget;
layout->addWidget(settingsContainer);
QBoxLayout* settingsLayout = new QVBoxLayout(settingsContainer);
settingsLayout->setContentsMargins(0, 0, 0, 0);
//
// VSync settings
_checkBoxVsync = new QCheckBox("Enable VSync");
_checkBoxVsync->setToolTip(
"If enabled the framerate will be locked to the refresh rate of the monitor"
);
_checkBoxVsync->setChecked(
_cluster.settings.has_value() &&
_cluster.settings->display.has_value() &&
_cluster.settings->display->swapInterval
);
settingsLayout->addWidget(_checkBoxVsync);
//
// Orientation specification
QLabel* labelOrientation = new QLabel("Orientation");
settingsLayout->addWidget(labelOrientation);
QWidget* orientationContainer = new QWidget;
settingsLayout->addWidget(orientationContainer);
QGridLayout* layoutWindow = new QGridLayout(orientationContainer);
sgct::quat orientation = sgct::quat(0.f, 0.f, 0.f, 0.f);
if (_cluster.scene.has_value() && _cluster.scene->orientation.has_value()) {
orientation = *_cluster.scene->orientation;
}
glm::quat q = glm::quat(orientation.w, orientation.x, orientation.y, orientation.z);
{
const QString pitchTip = "Pitch or elevation: negative numbers tilt the camera "
"downwards; positive numbers tilt upwards.\nThe allowed range is [-90, 90].";
QLabel* labelPitch = new QLabel("Pitch");
labelPitch->setToolTip(pitchTip);
layoutWindow->addWidget(labelPitch, 0, 0);
_linePitch = new QLineEdit;
_linePitch->setText(QString::number(glm::degrees(glm::pitch(q))));
_linePitch->setToolTip(pitchTip);
QDoubleValidator* validatorPitch = new QDoubleValidator(-90.0, 90.0, 15);
validatorPitch->setNotation(QDoubleValidator::StandardNotation);
_linePitch->setValidator(validatorPitch);
layoutWindow->addWidget(_linePitch, 0, 1);
QLabel* range = new QLabel("Range [-90, 90] in degrees");
layoutWindow->addWidget(range, 0, 2);
}
{
const QString rollTip = "Roll or bank: negative numbers rotate the camera "
"counter-clockwise; positive numbers clockwise.\nThe allowed range is "
"[-180, 180].";
QLabel* labelRoll = new QLabel("Roll");
labelRoll->setToolTip(rollTip);
layoutWindow->addWidget(labelRoll, 1, 0);
_lineRoll = new QLineEdit;
_lineRoll->setText(QString::number(glm::degrees(glm::roll(q))));
_lineRoll->setToolTip(rollTip);
QDoubleValidator* validatorRoll = new QDoubleValidator(-360.0, 360.0, 15);
validatorRoll->setNotation(QDoubleValidator::StandardNotation);
_lineRoll->setValidator(validatorRoll);
layoutWindow->addWidget(_lineRoll, 1, 1);
QLabel* range = new QLabel("Range [-360, 360] in degrees");
layoutWindow->addWidget(range, 1, 2);
}
{
const QString yawTip = "Yaw, heading, or azimuth: negative numbers pan the "
"camera to the left; positive numbers pan to the\nright. The allowed range "
"is [-360, 360].";
QLabel* labelYaw = new QLabel;
labelYaw->setText("Yaw");
labelYaw->setToolTip(yawTip);
layoutWindow->addWidget(labelYaw, 2, 0);
_lineYaw = new QLineEdit;
_lineYaw->setText(QString::number(glm::degrees(glm::yaw(q))));
_lineYaw->setToolTip(yawTip);
QDoubleValidator* validatorYaw = new QDoubleValidator(-180.0, 180.0, 15, this);
validatorYaw->setNotation(QDoubleValidator::StandardNotation);
_lineYaw->setValidator(validatorYaw);
layoutWindow->addWidget(_lineYaw, 2, 1);
QLabel* range = new QLabel("Range [-180, 180] in degrees");
layoutWindow->addWidget(range, 2, 2);
}
_settingsWidget = new SettingsWidget(orientation, this);
layout->addWidget(_settingsWidget);
connect(
_displayWidget, &DisplayWindowUnion::nWindowsChanged,
_settingsWidget, &SettingsWidget::nWindowsDisplayedChanged
);
connect(
_displayWidget, &DisplayWindowUnion::nWindowsChanged,
this, &SgctEdit::nWindowsDisplayedChanged
);
if (_settingsWidget->firstWindowGraphicsSelection()) {
connect(
_settingsWidget->firstWindowGraphicsSelection(),
&QComboBox::currentTextChanged,
this,
&SgctEdit::firstWindowGraphicsSelectionChanged
);
}
if (_settingsWidget->showUiOnFirstWindowCheckbox()) {
connect(
_settingsWidget->showUiOnFirstWindowCheckbox(),
&QCheckBox::clicked,
this,
&SgctEdit::firstWindowGuiOptionClicked
);
}
//
// Button box
{
QHBoxLayout* layoutButtonBox = new QHBoxLayout;
layoutButtonBox->addStretch(1);
@@ -333,35 +234,76 @@ void SgctEdit::createWidgets(const std::vector<QRect>& monitorSizes,
bottomBorder->setFrameShape(QFrame::HLine);
layout->addWidget(bottomBorder);
_cancelButton = new QPushButton("Cancel");
_cancelButton->setToolTip("Cancel changes");
_cancelButton->setFocusPolicy(Qt::NoFocus);
connect(_cancelButton, &QPushButton::released, this, &SgctEdit::reject);
layoutButtonBox->addWidget(_cancelButton);
QPushButton* cancelButton = new QPushButton("Cancel");
cancelButton->setToolTip("Cancel changes");
cancelButton->setFocusPolicy(Qt::NoFocus);
connect(cancelButton, &QPushButton::released, this, &SgctEdit::reject);
layoutButtonBox->addWidget(cancelButton);
_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);
layoutButtonBox->addWidget(_saveButton);
QPushButton* saveButton = new QPushButton("Save");
saveButton->setToolTip("Save configuration changes");
saveButton->setFocusPolicy(Qt::NoFocus);
connect(saveButton, &QPushButton::released, this, &SgctEdit::saveCluster);
layoutButtonBox->addWidget(saveButton);
_applyButton = new QPushButton("Apply Without Saving");
_applyButton->setToolTip("Apply configuration changes without saving to file");
_applyButton->setFocusPolicy(Qt::NoFocus);
connect(_applyButton, &QPushButton::released, this, &SgctEdit::apply);
layoutButtonBox->addWidget(_applyButton);
QPushButton* applyButton = new QPushButton("Apply Without Saving");
applyButton->setToolTip("Apply configuration changes without saving to file");
applyButton->setFocusPolicy(Qt::NoFocus);
connect(applyButton, &QPushButton::released, this, &SgctEdit::apply);
layoutButtonBox->addWidget(applyButton);
layout->addLayout(layoutButtonBox);
}
}
std::filesystem::path SgctEdit::saveFilename() const {
return _saveTarget;
return _configurationFilename;
}
void SgctEdit::save() {
generateConfiguration();
void SgctEdit::saveCluster() {
//
// Generate configuration
// Reconstitute the quaternion if the provided values are not 0
const float pitch = glm::radians(_linePitch->text().toFloat());
const float yaw = glm::radians(_lineYaw->text().toFloat());
const float roll = glm::radians(_lineRoll->text().toFloat());
if (pitch != 0.f && yaw != 0.f && roll != 0.f) {
glm::quat q = glm::quat(glm::vec3(pitch, yaw, roll));
_cluster.scene = {
.orientation = sgct::quat(q.x, q.y, q.z, q.w)
};
}
ghoul_assert(!_cluster.nodes.empty(), "There must be at least one node");
sgct::config::Node& node = _cluster.nodes.back();
//
// Generate vsync setup
if (_checkBoxVsync->isChecked()) {
if (!_cluster.settings || !_cluster.settings->display ||
!_cluster.settings->display->swapInterval)
{
_cluster.settings = sgct::config::Settings{
.display = sgct::config::Settings::Display {
.swapInterval = 1
}
};
}
}
else {
_cluster.settings = std::nullopt;
}
_displayWidget->applyWindowSettings(node.windows);
//
// Generate individual window settings
for (size_t i = 0; i < node.windows.size(); i++) {
// First apply default settings to each window
node.windows[i].id = static_cast<int8_t>(i);
node.windows[i].viewports.back().isTracked = true;
}
if (hasWindowIssues(_cluster)) {
const int ret = QMessageBox::warning(
this,
@@ -378,11 +320,7 @@ void SgctEdit::save() {
}
}
if (_didImportValues) {
_saveTarget = _configurationFilename;
accept();
}
else {
if (_configurationFilename.empty()) {
const QString fileName = QFileDialog::getSaveFileName(
this,
"Save Window Configuration File",
@@ -392,201 +330,42 @@ void SgctEdit::save() {
#ifdef __linux__
// Linux in Qt5 and Qt6 crashes when trying to access the native dialog here
, QFileDialog::DontUseNativeDialog
#endif
#endif // __linux__
);
if (!fileName.isEmpty()) {
_saveTarget = fileName.toStdString();
accept();
if (fileName.isEmpty()) {
return;
}
_configurationFilename = fileName.toStdString();
}
//
// Save the cluster configuration
ghoul_assert(!_configurationFilename.empty(), "Filename must not be empty");
std::ofstream outFile;
outFile.open(_configurationFilename, std::ofstream::out);
if (outFile.good()) {
sgct::config::GeneratorVersion genEntry = VersionMin;
outFile << sgct::serializeConfig(_cluster, genEntry);
accept();
}
else {
QMessageBox::critical(
this,
"Exception",
QString::fromStdString(std::format(
"Error writing data to file '{}'", _configurationFilename
))
);
}
}
void SgctEdit::apply() {
generateConfiguration();
if (hasWindowIssues(_cluster)) {
const int ret = QMessageBox::warning(
this,
"Window Sizes Incompatible",
"Window sizes for multiple windows have to be strictly ordered, meaning that "
"the size of window 1 has to be bigger in each dimension than window 2, "
"window 2 has to be bigger than window 3 (if it exists), and window 3 has to "
"be bigger than window 4.\nOtherwise, rendering errors might occur.\n\nAre "
"you sure you want to continue?",
QMessageBox::Yes | QMessageBox::No
);
if (ret == QMessageBox::No) {
return;
}
}
std::string userCfgTempDir = _userConfigPath.string();
if (userCfgTempDir.back() != '/') {
userCfgTempDir += '/';
}
userCfgTempDir += "temp";
if (!std::filesystem::is_directory(absPath(userCfgTempDir))) {
std::filesystem::create_directories(absPath(userCfgTempDir));
}
_saveTarget = userCfgTempDir + "/apply-without-saving.json";
accept();
}
void SgctEdit::generateConfiguration() {
_cluster.scene = sgct::config::Scene();
_cluster.scene->orientation = _settingsWidget->orientation();
if (_cluster.nodes.empty()) {
_cluster.nodes.emplace_back();
}
sgct::config::Node& node = _cluster.nodes.back();
generateConfigSetupVsync();
generateConfigUsers();
generateConfigAddresses(node);
generateConfigResizeWindowsAccordingToSelected(node);
generateConfigIndividualWindowSettings(node);
}
void SgctEdit::generateConfigSetupVsync() {
if (_settingsWidget->vsync()) {
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;
}
}
void SgctEdit::generateConfigUsers() {
if (!_didImportValues) {
sgct::config::User user;
user.eyeSeparation = 0.065f;
user.position = { 0.f, 0.f, 0.f };
_cluster.users = { user };
}
}
void SgctEdit::generateConfigAddresses(sgct::config::Node& node) {
if (!_didImportValues) {
_cluster.masterAddress = "localhost";
node.address = "localhost";
node.port = 20401;
}
}
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.emplace_back();
}
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 = static_cast<int>(i);
node.windows[i].draw2D = true;
node.windows[i].draw3D = true;
node.windows[i].viewports.back().isTracked = true;
deleteFromTags(node.windows[i]);
// 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].tags.emplace_back("GUI");
const int selectedGraphics =
_settingsWidget->graphicsSelectionForShowUiOnFirstWindow();
if (selectedGraphics == 0) {
node.windows[i].viewports.back().isTracked = false;
node.windows[i].tags.emplace_back("GUI_No_Render");
}
else if (selectedGraphics > 0 &&
selectedGraphics <= static_cast<int>(node.windows.size()))
{
node.windows[i].tags.emplace_back(
"GUI_Render_Win" + std::to_string(selectedGraphics)
);
// Set first window viewport to mirror the selected window's viewport
node.windows[i].viewports =
node.windows[(selectedGraphics - 1)].viewports;
}
node.windows[i].draw2D = true;
node.windows[i].draw3D = (selectedGraphics > 0);
}
else {
node.windows[i].draw2D = false;
node.windows[i].draw3D = true;
}
}
}
}
void SgctEdit::deleteFromTags(sgct::config::Window& window) {
constexpr std::array<std::string_view, 6> Tags = {
"GUI",
"GUI_No_Render",
"GUI_Render_Win1",
"GUI_Render_Win2",
"GUI_Render_Win3",
"GUI_Render_Win4"
};
for (const std::string_view tag : Tags) {
window.tags.erase(
std::remove(window.tags.begin(), window.tags.end(), tag),
window.tags.end()
);
}
}
sgct::config::Cluster SgctEdit::cluster() const {
return _cluster;
}
void SgctEdit::firstWindowGraphicsSelectionChanged(const QString&) {
if (!_settingsWidget) {
return;
}
if (_settingsWidget->showUiOnFirstWindow()) {
const int newSetting = _settingsWidget->graphicsSelectionForShowUiOnFirstWindow();
_displayWidget->activeWindowControls()[0]->setVisibilityOfProjectionGui(
newSetting == 1
);
}
}
void SgctEdit::nWindowsDisplayedChanged(int newCount) {
if (!_settingsWidget) {
return;
}
if (newCount == 1) {
_displayWidget->activeWindowControls()[0]->setVisibilityOfProjectionGui(true);
}
else {
firstWindowGraphicsSelectionChanged("");
}
}
void SgctEdit::firstWindowGuiOptionClicked(bool checked) {
if (checked) {
firstWindowGraphicsSelectionChanged("");
}
else {
_displayWidget->activeWindowControls()[0]->setVisibilityOfProjectionGui(true);
std::filesystem::path userTmp = _userConfigPath / "temp";
if (!std::filesystem::is_directory(absPath(userTmp))) {
std::filesystem::create_directories(absPath(userTmp));
}
_configurationFilename = (userTmp / "apply-without-saving.json").string();
saveCluster();
}

View File

@@ -27,7 +27,7 @@
#include <ghoul/format.h>
#include <ghoul/misc/assert.h>
#include "sgctedit/displaywindowunion.h"
#include "sgctedit/monitorbox.h"
#include "windowcolors.h"
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
@@ -39,34 +39,35 @@
#include <numbers>
namespace {
std::array<std::string, 4> MonitorNames = {
"Primary", "Secondary", "Tertiary", "Quaternary"
std::array<std::pair<int, std::string>, 10> Quality = {
std::pair{ 256, "Low (256)" },
std::pair{ 512, "Medium (512)" },
std::pair{ 1024, "High (1K)" },
std::pair{ 1536, "1.5K (1536)" },
std::pair{ 2048, "2K (2048)" },
std::pair{ 4096, "4K (4096)" },
std::pair{ 8192, "8K (8192)" },
std::pair{ 16384, "16K (16384)" },
std::pair{ 32768, "32K (32768)" },
std::pair{ 65536, "64K (65536)" }
};
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 std::array<QRect, 4> DefaultWindowSizes = {
QRect(50, 50, 1280, 720),
QRect(150, 150, 1280, 720),
QRect(50, 50, 1280, 720),
QRect(150, 150, 1280, 720)
};
constexpr std::array<int, nQualityTypes> QualityValues = {
256, 512, 1024, 1536, 2048, 4096, 8192, 16384, 32768, 65536
enum class ProjectionIndices {
Planar = 0,
Fisheye,
SphericalMirror,
Cylindrical,
Equirectangular
};
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),
QRectF(50.f, 50.f, 1280.f, 720.f),
QRectF(150.f, 150.f, 1280.f, 720.f)
};
constexpr int LineEditWidthFixedWindowSize = 64;
constexpr int LineEditWidthFixedWindowSize = 95;
constexpr float DefaultFovLongEdge = 80.f;
constexpr float DefaultFovShortEdge = 50.534f;
constexpr float DefaultHeightOffset = 0.f;
@@ -74,21 +75,46 @@ namespace {
constexpr double FovEpsilon = 0.00001;
QList<QString> monitorNames(const std::vector<QRect>& resolutions) {
std::array<std::string, 4> MonitorNames = {
"Primary", "Secondary", "Tertiary", "Quaternary"
};
QList<QString> monitorNames;
for (size_t i = 0; i < resolutions.size(); i++) {
const std::string name = i < 4 ? MonitorNames[i] : std::format("{}th", i);
const std::string fullName = std::format(
"{} ({}x{})",
MonitorNames[i], resolutions[i].width(), resolutions[i].height()
name,
resolutions[i].width(),
resolutions[i].height()
);
monitorNames.push_back(QString::fromStdString(fullName));
}
return monitorNames;
}
QStringList qualityList() {
QStringList res;
for (const std::pair<int, std::string>& p : Quality) {
res.append(QString::fromStdString(p.second));
}
return res;
}
int indexForQuality(int quality) {
auto it = std::find_if(
Quality.cbegin(),
Quality.cend(),
[quality](const std::pair<int, std::string>& p) { return p.first == quality; }
);
ghoul_assert(it != Quality.cend(), "Combobox has too many values");
return std::distance(Quality.cbegin(), it);
}
} // namespace
WindowControl::WindowControl(int monitorIndex, int windowIndex,
const std::vector<QRect>& monitorDims,
const QColor& winColor, bool resetToDefault, QWidget* parent)
const std::vector<QRect>& monitorDims, QWidget* parent)
: QWidget(parent)
, _monitorIndexDefault(monitorIndex)
, _windowIndex(windowIndex)
@@ -96,13 +122,6 @@ WindowControl::WindowControl(int monitorIndex, int windowIndex,
, _lockIcon(":/images/outline_locked.png")
, _unlockIcon(":/images/outline_unlocked.png")
{
createWidgets(winColor);
if (resetToDefault) {
resetToDefaults();
}
}
void WindowControl::createWidgets(const QColor& windowColor) {
// Col 0 Col 1 Col 2 Col 3 Col 4 Col 5 Col 6 Col 7
// *----------*----------*-------*----------*-------*--------*-------*-------*
// | Window {n} | R0
@@ -110,8 +129,8 @@ void WindowControl::createWidgets(const QColor& windowColor) {
// | Monitor * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD> | R2
// | Size * [xxxxxx] * x * [yyyyyy] * px * <lock> * < Set to | R3
// | Offset * [xxxxxx] * , * [yyyyyy] * px * * Fullscreen> | R4
// | [] Window Decoration | R5
// | [] UI only in this window | R6
// | [] Window Decoration [] Render 2D | R5
// | [] Spout Output [] Render 3D | R6
// | ~~~~~~~~~~~~~~~~~~~~~~~~~Projection components~~~~~~~~~~~~~~~~~~~~~~~~~ | R7
// *----------*----------*-------*----------*-------*--------*-------*-------*
@@ -120,13 +139,19 @@ void WindowControl::createWidgets(const QColor& windowColor) {
layout->setContentsMargins(margins.left(), 0, margins.right(), 0);
layout->setColumnStretch(6, 1);
layout->setRowStretch(8, 1);
//
// Window title
_windowNumber = new QLabel("Window " + QString::number(_windowIndex + 1));
QColor windowColor = colorForWindow(_windowIndex);
_windowNumber->setStyleSheet(QString::fromStdString(std::format(
"QLabel {{ color : #{:02x}{:02x}{:02x}; }}",
windowColor.red(), windowColor.green(), windowColor.blue()
)));
layout->addWidget(_windowNumber, 0, 0, 1, 8, Qt::AlignCenter);
//
// Name
{
const QString tip = "The name for the window (displayed in title bar)";
@@ -140,6 +165,8 @@ void WindowControl::createWidgets(const QColor& windowColor) {
}
const QString tip = "The monitor where this window is located";
//
// Monitor
_monitor = new QComboBox;
_monitor->addItems(monitorNames(_monitorResolutions));
_monitor->setCurrentIndex(_monitorIndexDefault);
@@ -155,12 +182,18 @@ void WindowControl::createWidgets(const QColor& windowColor) {
}
);
if (_monitorResolutions.size() > 1) {
// We only add the monitor dropdown menu if we are running on a system with
// multiple monitors. We are still creating the combobox to guard against
// potential nullpointer accesses elsewhere in the code
QLabel* labelLocation = new QLabel("Monitor");
labelLocation->setToolTip(tip);
layout->addWidget(labelLocation, 2, 0);
layout->addWidget(_monitor, 2, 1, 1, 7);
}
//
// Window size
{
QLabel* size = new QLabel("Size");
size->setToolTip("The window's width & height in pixels");
@@ -213,8 +246,11 @@ void WindowControl::createWidgets(const QColor& windowColor) {
}
);
}
//
// Position
{
QLabel* offset = new QLabel("Offset");
QLabel* offset = new QLabel("Position");
offset->setToolTip(
"The x,y location of the window's upper left corner from monitor's "
"upper-left corner origin (pixels)"
@@ -258,9 +294,10 @@ void WindowControl::createWidgets(const QColor& windowColor) {
unit->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum));
layout->addWidget(unit, 4, 4, Qt::AlignLeft);
}
{
QBoxLayout* holderLayout = new QHBoxLayout;
//
// Fullscreen button
{
QPushButton* setFullscreen = new QPushButton("Set Window\nto Fullscreen");
setFullscreen->setToolTip(
"If enabled, the window will be created in an exclusive fullscreen mode. The "
@@ -272,10 +309,7 @@ void WindowControl::createWidgets(const QColor& windowColor) {
QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding
);
holderLayout->addStretch();
holderLayout->addWidget(setFullscreen);
holderLayout->addStretch();
layout->addLayout(holderLayout, 3, 6, 2, 2);
layout->addWidget(setFullscreen, 3, 6, 2, 2);
connect(
setFullscreen, &QPushButton::released,
this, &WindowControl::onFullscreenClicked
@@ -287,8 +321,11 @@ void WindowControl::createWidgets(const QColor& windowColor) {
"If disabled, the window will not have a border frame or title bar, and no\n "
"controls for minimizing/maximizing, resizing, or closing the window"
);
layout->addWidget(_windowDecoration, 5, 0, 1, 8);
layout->addWidget(_windowDecoration, 5, 0, 1, 4);
}
//
// Spout output
{
_spoutOutput = new QCheckBox("Spout Output");
_spoutOutput->setToolTip(
@@ -297,35 +334,51 @@ void WindowControl::createWidgets(const QColor& windowColor) {
"images available to other real-time applications on the same machine for "
"further processing"
);
layout->addWidget(_spoutOutput);
layout->addWidget(_spoutOutput, 6, 0, 1, 4);
}
//
// Render 2D & 3D
{
QFrame* projectionGroup = new QFrame;
projectionGroup->setVisible(true);
projectionGroup->setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
_render2D = new QCheckBox("Render GUI and Overlays");
_render2D->setToolTip(
"Determines whether any overlays should be\nrendered in this window. "
"Overlays in this case are\nthe user interface, dashboard information, and "
"other\nelements that are only useful for a pilot."
);
layout->addWidget(_render2D, 5, 4, 1, 4);
_render3D = new QCheckBox("Render Scene");
_render3D->setToolTip(
"Determines whether the main 3D scene should be\nrendered in this window, "
"like 3D models, the planets, stars, etc."
);
connect(
_render3D, &QCheckBox::clicked,
[this](bool checked) {
_projectionGroup->setVisible(checked);
}
);
layout->addWidget(_render3D, 6, 4, 1, 4);
}
//
// Projections
{
_projectionGroup = new QFrame;
_projectionGroup->setVisible(true);
_projectionGroup->setFrameStyle(QFrame::StyledPanel | QFrame::Plain);
//
// Projection combobox
QBoxLayout* projectionLayout = new QVBoxLayout(projectionGroup);
QBoxLayout* projectionLayout = new QVBoxLayout(_projectionGroup);
projectionLayout->setContentsMargins(0, 0, 0, 0);
projectionLayout->setSpacing(0);
_projectionLabel = new QLabel(
"Projection information not shown while user interface is set to display "
"on the first window only"
);
_projectionLabel->setWordWrap(true);
_projectionLabel->setObjectName("notice");
_projectionLabel->setVisible(false);
_projectionLabel->setEnabled(false);
projectionLayout->addWidget(_projectionLabel);
_projectionType = new QComboBox;
_projectionType->addItems({
ProjectionTypes[0],
ProjectionTypes[1],
ProjectionTypes[2],
ProjectionTypes[3],
ProjectionTypes[4]
"Planar Projection", "Fisheye", "Spherical Mirror Projection",
"Cylindrical Projection", "Equirectangular Projection"
});
_projectionType->setToolTip("Select from the supported window projection types");
_projectionType->setCurrentIndex(0);
@@ -353,8 +406,10 @@ void WindowControl::createWidgets(const QColor& windowColor) {
// We need to trigger this once to ensure that all of the defaults are correct
onProjectionChanged(0);
layout->addWidget(projectionGroup, 7, 0, 1, 8);
layout->addWidget(_projectionGroup, 7, 0, 1, 8);
}
resetToDefaults();
}
QWidget* WindowControl::createPlanarWidget() {
@@ -373,7 +428,7 @@ QWidget* WindowControl::createPlanarWidget() {
"This projection type is the 'regular' projection with a horizontal and a "
"vertical field of view, given in degrees. The wider the field of view, the "
"more content is shown at the same time, but everything becomes smaller. Very "
"large values will introduce distorions on the corners"
"large values will introduce distortions on the corners."
);
_planar.labelInfo->setObjectName("info");
_planar.labelInfo->setWordWrap(true);
@@ -449,7 +504,7 @@ QWidget* WindowControl::createFisheyeWidget() {
"This projection provides a rendering in a format that is suitable for "
"planetariums and other immersive environments. A field-of-view of 180 degrees "
"is presented as a circular image in the center of the screen. For this "
"projection a square window is suggested, but not necessary"
"projection a square window is suggested, but not necessary."
);
_fisheye.labelInfo->setObjectName("info");
_fisheye.labelInfo->setWordWrap(true);
@@ -463,7 +518,7 @@ QWidget* WindowControl::createFisheyeWidget() {
layout->addWidget(_fisheye.labelQuality, 1, 0);
_fisheye.quality = new QComboBox;
_fisheye.quality->addItems(QualityTypes);
_fisheye.quality->addItems(qualityList());
_fisheye.quality->setToolTip(qualityTip);
_fisheye.quality->setCurrentIndex(2);
layout->addWidget(_fisheye.quality, 1, 1);
@@ -485,7 +540,7 @@ QWidget* WindowControl::createSphericalMirrorWidget() {
"This projection is rendering a image suite for use with a spherical mirror "
"projection as described by Paul Bourke (http://paulbourke.net/dome/mirrordome/) "
"and which is a low-cost yet effective way to provide content for a sphericalal "
"display surface using a regular projector"
"display surface using a regular projector."
);
_sphericalMirror.labelInfo->setObjectName("info");
_sphericalMirror.labelInfo->setWordWrap(true);
@@ -499,7 +554,7 @@ QWidget* WindowControl::createSphericalMirrorWidget() {
layout->addWidget(_sphericalMirror.labelQuality, 1, 0);
_sphericalMirror.quality = new QComboBox;
_sphericalMirror.quality->addItems(QualityTypes);
_sphericalMirror.quality->addItems(qualityList());
_sphericalMirror.quality->setToolTip(qualityTip);
_sphericalMirror.quality->setCurrentIndex(2);
layout->addWidget(_sphericalMirror.quality, 1, 1);
@@ -522,7 +577,7 @@ QWidget* WindowControl::createCylindricalWidget() {
"This projection type provides a cylindrical rendering that covers 360 degrees "
"around the camera, which can be useful in immersive environments that are not "
"spherical, but where, for example, all walls of a room are covered with "
"projectors"
"projectors."
);
_cylindrical.labelInfo->setObjectName("info");
_cylindrical.labelInfo->setWordWrap(true);
@@ -536,7 +591,7 @@ QWidget* WindowControl::createCylindricalWidget() {
layout->addWidget(_cylindrical.labelQuality, 1, 0);
_cylindrical.quality = new QComboBox;
_cylindrical.quality->addItems(QualityTypes);
_cylindrical.quality->addItems(qualityList());
_cylindrical.quality->setToolTip(qualityTip);
_cylindrical.quality->setCurrentIndex(2);
layout->addWidget(_cylindrical.quality, 1, 1);
@@ -575,7 +630,7 @@ QWidget* WindowControl::createEquirectangularWidget() {
"This projection provides the rendering as an image in equirectangular "
"projection, which is a common display type for 360 surround video. When "
"uploading a video in equirectangular projection to YouTube, for example, it "
"will use it as a 360 video"
"will use it as a 360 video."
);
_equirectangular.labelInfo->setObjectName("info");
_equirectangular.labelInfo->setWordWrap(true);
@@ -589,7 +644,7 @@ QWidget* WindowControl::createEquirectangularWidget() {
layout->addWidget(_equirectangular.labelQuality, 1, 0);
_equirectangular.quality = new QComboBox;
_equirectangular.quality->addItems(QualityTypes);
_equirectangular.quality->addItems(qualityList());
_equirectangular.quality->setToolTip(qualityTip);
_equirectangular.quality->setCurrentIndex(2);
layout->addWidget(_equirectangular.quality, 1, 1);
@@ -622,6 +677,8 @@ void WindowControl::resetToDefaults() {
}
_windowDecoration->setChecked(true);
_spoutOutput->setChecked(false);
_render2D->setChecked(true);
_render3D->setChecked(true);
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Planar));
_planar.fovV->setValue(DefaultFovLongEdge);
_planar.fovV->setValue(DefaultFovShortEdge);
@@ -633,12 +690,12 @@ 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::setDimensions(int x, int y, int width, int height) {
_windowDimensions = QRect(x, y, width, height);
_sizeX->setValue(width);
_sizeY->setValue(height);
_offsetX->setValue(x);
_offsetY->setValue(y);
}
void WindowControl::setMonitorSelection(int monitorIndex) {
@@ -661,40 +718,69 @@ void WindowControl::setSpoutOutputState(bool shouldSpoutOutput) {
_spoutOutput->setChecked(shouldSpoutOutput);
}
sgct::config::Projections WindowControl::generateProjectionInformation() const {
const ProjectionIndices type =
static_cast<WindowControl::ProjectionIndices>(_projectionType->currentIndex());
void WindowControl::setRender2D(bool state) {
_render2D->setChecked(state);
}
switch (type) {
void WindowControl::setRender3D(bool state) {
_render3D->setChecked(state);
}
void WindowControl::generateWindowInformation(sgct::config::Window& window) const {
window.size = { _sizeX->value(), _sizeY->value() };
window.monitor = _monitor->currentIndex();
const QRect resolution = _monitorResolutions[_monitor->currentIndex()];
window.pos = sgct::ivec2(
resolution.x() + _offsetX->value(),
resolution.y() + _offsetY->value()
);
window.draw2D = _render2D->isChecked();
window.draw3D = _render3D->isChecked();
window.isDecorated = _windowDecoration->isChecked();
if (_spoutOutput->isChecked()) {
window.spout = sgct::config::Window::Spout{
.enabled = true
};
}
if (!_windowName->text().isEmpty()) {
window.name = _windowName->text().toStdString();
}
// The rest of this function is just specifying the rendering, which we can skip if we
// don't want to render 3D anyway
if (!window.draw3D) {
return;
}
sgct::config::Viewport vp;
vp.isTracked = true;
vp.position = sgct::vec2(0.f, 0.f);
vp.size = sgct::vec2(1.f, 1.f);
switch (static_cast<ProjectionIndices>(_projectionType->currentIndex())) {
case ProjectionIndices::Fisheye:
{
sgct::config::FisheyeProjection projection;
projection.quality = QualityValues[_fisheye.quality->currentIndex()];
projection.fov = 180.f;
projection.tilt = 0.f;
return projection;
}
vp.projection = sgct::config::FisheyeProjection {
.fov = 180.f,
.quality = Quality[_fisheye.quality->currentIndex()].first,
.tilt = 0.f
};
break;
case ProjectionIndices::SphericalMirror:
{
sgct::config::SphericalMirrorProjection projection;
projection.quality =
QualityValues[_sphericalMirror.quality->currentIndex()];
return projection;
}
vp.projection = sgct::config::SphericalMirrorProjection {
.quality = Quality[_sphericalMirror.quality->currentIndex()].first
};
break;
case ProjectionIndices::Cylindrical:
{
sgct::config::CylindricalProjection projection;
projection.quality = QualityValues[_cylindrical.quality->currentIndex()];
projection.heightOffset = _cylindrical.heightOffset->text().toFloat();
return projection;
}
vp.projection = sgct::config::CylindricalProjection {
.quality = Quality[_cylindrical.quality->currentIndex()].first,
.heightOffset = static_cast<float>(_cylindrical.heightOffset->value())
};
break;
case ProjectionIndices::Equirectangular:
{
sgct::config::EquirectangularProjection projection;
projection.quality =
QualityValues[_equirectangular.quality->currentIndex()];
return projection;
}
vp.projection = sgct::config::EquirectangularProjection {
.quality = Quality[_equirectangular.quality->currentIndex()].first
};
break;
case ProjectionIndices::Planar:
{
double fovH = _planar.fovH->value();
@@ -709,38 +795,13 @@ sgct::config::Projections WindowControl::generateProjectionInformation() const {
projection.fov.left = -projection.fov.right;
projection.fov.up = fovV / 2.0;
projection.fov.down = -projection.fov.up;
return projection;
vp.projection = projection;
break;
}
default:
throw ghoul::MissingCaseException();
}
}
void WindowControl::generateWindowInformation(sgct::config::Window& window) const {
window.size = { _sizeX->text().toInt(), _sizeY->text().toInt() };
window.monitor = _monitor->currentIndex();
const QRect resolution = _monitorResolutions[_monitor->currentIndex()];
window.pos = sgct::ivec2(
resolution.x() + _offsetX->text().toInt(),
resolution.y() + _offsetY->text().toInt()
);
sgct::config::Viewport vp;
vp.isTracked = true;
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 (_spoutOutput->isChecked()) {
window.spout = sgct::config::Window::Spout();
window.spout->enabled = true;
}
if (!_windowName->text().isEmpty()) {
window.name = _windowName->text().toStdString();
}
}
void WindowControl::setProjectionPlanar(float hfov, float vfov) {
@@ -750,66 +811,30 @@ void WindowControl::setProjectionPlanar(float hfov, float vfov) {
}
void WindowControl::setProjectionFisheye(int quality) {
setQualityComboBoxFromLinesResolution(quality, _fisheye.quality);
_fisheye.quality->setCurrentIndex(indexForQuality(quality));
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Fisheye));
}
void WindowControl::setProjectionSphericalMirror(int quality) {
setQualityComboBoxFromLinesResolution(quality, _sphericalMirror.quality);
_sphericalMirror.quality->setCurrentIndex(indexForQuality(quality));
_projectionType->setCurrentIndex(
static_cast<int>(ProjectionIndices::SphericalMirror)
);
}
void WindowControl::setProjectionCylindrical(int quality, float heightOffset) {
setQualityComboBoxFromLinesResolution(quality, _cylindrical.quality);
_cylindrical.quality->setCurrentIndex(indexForQuality(quality));
_cylindrical.heightOffset->setValue(heightOffset);
_projectionType->setCurrentIndex(static_cast<int>(ProjectionIndices::Cylindrical));
}
void WindowControl::setProjectionEquirectangular(int quality) {
setQualityComboBoxFromLinesResolution(quality, _equirectangular.quality);
_equirectangular.quality->setCurrentIndex(indexForQuality(quality));
_projectionType->setCurrentIndex(
static_cast<int>(ProjectionIndices::Equirectangular)
);
}
void WindowControl::setVisibilityOfProjectionGui(bool enable) {
_projectionType->setVisible(enable);
_planar.labelInfo->setVisible(enable);
_planar.fovH->setVisible(enable);
_planar.labelFovH->setVisible(enable);
_planar.fovV->setVisible(enable);
_planar.labelFovV->setVisible(enable);
_planar.buttonLockFov->setVisible(enable);
_fisheye.labelInfo->setVisible(enable);
_fisheye.quality->setVisible(enable);
_fisheye.labelQuality->setVisible(enable);
_sphericalMirror.labelInfo->setVisible(enable);
_sphericalMirror.quality->setVisible(enable);
_sphericalMirror.labelQuality->setVisible(enable);
_cylindrical.labelInfo->setVisible(enable);
_cylindrical.heightOffset->setVisible(enable);
_cylindrical.labelHeightOffset->setVisible(enable);
_cylindrical.quality->setVisible(enable);
_cylindrical.labelQuality->setVisible(enable);
_equirectangular.labelInfo->setVisible(enable);
_equirectangular.quality->setVisible(enable);
_equirectangular.labelQuality->setVisible(enable);
_projectionLabel->setVisible(!enable);
}
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) {
_windowDimensions.setWidth(newValue);
if (_aspectRatioLocked) {
@@ -876,34 +901,28 @@ void WindowControl::onProjectionChanged(int newSelection) const {
void WindowControl::onAspectRatioLockClicked() {
_aspectRatioLocked = !_aspectRatioLocked;
if (_aspectRatioLocked) {
_aspectRatioSize = _windowDimensions.width() / _windowDimensions.height();
const float w = static_cast<float>(_windowDimensions.width());
const float h = static_cast<float>(_windowDimensions.height());
_aspectRatioSize = w / h;
}
}
void WindowControl::onFovLockClicked() {
_fovLocked = !_fovLocked;
_planar.fovH->setEnabled(!_fovLocked);
_planar.fovV->setEnabled(!_fovLocked);
if (_fovLocked) {
_planar.fovH->setEnabled(false);
_planar.fovV->setEnabled(false);
updatePlanarLockedFov();
}
else {
_planar.fovH->setEnabled(true);
_planar.fovV->setEnabled(true);
}
_planar.buttonLockFov->setIcon(_fovLocked ? _lockIcon : _unlockIcon);
}
void WindowControl::updatePlanarLockedFov() {
const float w = static_cast<float>(_windowDimensions.width());
const float h = static_cast<float>(_windowDimensions.height());
const bool landscapeOrientation =
(_windowDimensions.width() >= _windowDimensions.height());
float aspectRatio = 0.f;
if (landscapeOrientation) {
aspectRatio = _windowDimensions.width() / _windowDimensions.height();
}
else {
aspectRatio = _windowDimensions.height() / _windowDimensions.width();
}
const float aspectRatio = landscapeOrientation ? (w / h) : (h / w);
float adjustedFov = 2.f * std::atan(aspectRatio * std::tan(DefaultFovShortEdge
* std::numbers::pi_v<float> / 180.f / 2.f));

View File

@@ -0,0 +1,165 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "splitcombobox.h"
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/misc/assert.h>
#include <QStandardItemModel>
SplitComboBox::SplitComboBox(QWidget* parent, std::filesystem::path userPath,
std::string userHeader, std::filesystem::path hardcodedPath,
std::string hardcodedHeader, std::string specialFirst,
std::function<bool(const std::filesystem::path&)> fileFilter,
std::function<std::string(const std::filesystem::path&)> createTooltip)
: QComboBox(parent)
, _userPath(std::move(userPath))
, _userHeader(std::move(userHeader))
, _hardCodedPath(std::move(hardcodedPath))
, _hardCodedHeader(std::move(hardcodedHeader))
, _specialFirst(std::move(specialFirst))
, _fileFilter(std::move(fileFilter))
, _createTooltip(std::move(createTooltip))
{
connect(
this,
QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) {
if (!_specialFirst.empty() && index == 0) {
// We have a special entry which is at the top of the list and that entry
// was selected
emit selectionChanged(std::nullopt);
}
else {
std::string path = itemData(index).toString().toStdString();
emit selectionChanged(std::move(path));
}
}
);
}
void SplitComboBox::populateList(const std::string& preset) {
// We don't want any signals to be fired while we are manipulating the list
blockSignals(true);
// Clear the previously existing entries since we might call this function again
clear();
//
// Special item (if it was specified)
if (!_specialFirst.empty()) {
addItem(
QString::fromStdString(_specialFirst),
QString::fromStdString(_specialFirst)
);
}
//
// User entries
addItem(QString::fromStdString(_userHeader));
qobject_cast<QStandardItemModel*>(model())->item(count() - 1)->setEnabled(false);
std::vector<std::filesystem::path> userFiles = ghoul::filesystem::walkDirectory(
_userPath,
ghoul::filesystem::Recursive::Yes,
ghoul::filesystem::Sorted::Yes,
_fileFilter
);
for (const std::filesystem::path& p : userFiles) {
std::filesystem::path relPath = std::filesystem::relative(p, _userPath);
relPath.replace_extension();
// Display the relative path, but store the full path in the user data segment
addItem(
QString::fromStdString(relPath.string()),
QString::fromStdString(p.string())
);
const QString toolTip = QString::fromStdString(std::format(
"<p>{}</p>",
_createTooltip ? _createTooltip(p) : "(no description available)"
));
setItemData(count() - 1, toolTip, Qt::ToolTipRole);
}
//
// Hardcoded entries
addItem(QString::fromStdString(_hardCodedHeader));
qobject_cast<QStandardItemModel*>(model())->item(count() - 1)->setEnabled(false);
std::vector<std::filesystem::path> hcFiles = ghoul::filesystem::walkDirectory(
_hardCodedPath,
ghoul::filesystem::Recursive::Yes,
ghoul::filesystem::Sorted::Yes,
_fileFilter
);
for (const std::filesystem::path& p : hcFiles) {
std::filesystem::path relPath = std::filesystem::relative(p, _hardCodedPath);
relPath.replace_extension();
// Display the relative path, but store the full path in the user data segment
addItem(
QString::fromStdString(relPath.string()),
QString::fromStdString(p.string())
);
std::string tooltipDescription = "(no description available)";
if (_createTooltip) {
tooltipDescription = _createTooltip(p);
}
const QString toolTip = QString::fromStdString(
std::format("<p>{}</p>", tooltipDescription)
);
setItemData(count() - 1, toolTip, Qt::ToolTipRole);
}
// Now we are ready to send signals again
blockSignals(false);
//
// Find the provided preset and set it as the current one
const int idx = findText(QString::fromStdString(preset));
if (idx != -1) {
// Found the preset, set it as the current index
setCurrentIndex(idx);
}
else {
// The preset wasn't found, so we add it to the top
std::string text = std::format("'{}' not found", preset);
insertItem(0, QString::fromStdString(text));
setCurrentIndex(0);
}
}
std::pair<std::string, std::string> SplitComboBox::currentSelection() const {
return {
currentText().toStdString(),
currentData().toString().toStdString()
};
}

View File

@@ -0,0 +1,50 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include "windowcolors.h"
#include <ghoul/misc/assert.h>
QColor colorForWindow(int idx) {
constexpr std::array<QColor, 4> Hardcoded = {
QColor(43, 158, 195),
QColor(252, 171, 16),
QColor(68, 175, 105),
QColor(248, 51, 60)
};
ghoul_assert(idx >= 0, "idx must be non-negative");
if (idx < 4) {
return Hardcoded[idx];
}
else {
// If someone asks for a color of an index greater than what we have, we generate
// them by multiplying some prime numbers together
const int r = (idx * 31 * 3083) % 256;
const int g = (idx * 43 * 4273) % 256;
const int b = (idx * 59 * 5857) % 256;
return QColor(r, g, b);
}
}

View File

@@ -1063,69 +1063,6 @@ void setSgctDelegateFunctions() {
};
}
std::string setWindowConfigPresetForGui(const std::string& labelFromCfgFile,
bool haveCliSGCTConfig)
{
openspace::Configuration& config = *global::configuration;
std::string preset;
const bool sgctCfgFileSpecifiedByLua = !config.sgctConfigNameInitialized.empty();
if (haveCliSGCTConfig) {
preset = std::format("{} (from CLI)", config.windowConfiguration);
}
else if (sgctCfgFileSpecifiedByLua) {
preset = config.sgctConfigNameInitialized + labelFromCfgFile;
}
else {
preset = config.windowConfiguration;
}
return preset;
}
std::pair<std::string, bool> selectedSgctProfileFromLauncher(LauncherWindow& lw,
bool hasCliSGCTConfig,
const std::string& windowConfiguration,
const std::string& labelFromCfgFile)
{
std::string config = windowConfiguration;
bool isGeneratedConfig = false;
if (!hasCliSGCTConfig) {
config = lw.selectedWindowConfig();
if (config.find(labelFromCfgFile) != std::string::npos) {
if (config.find("sgct.config") == std::string::npos) {
config = config.substr(0, config.length() - labelFromCfgFile.length());
}
else {
config = windowConfiguration;
isGeneratedConfig = true;
}
}
else {
const std::filesystem::path c = absPath(config);
std::filesystem::path cj = c;
cj.replace_extension(".json");
if (c.extension().empty()) {
if (std::filesystem::exists(cj)) {
config += ".json";
}
else {
throw ghoul::RuntimeError(std::format(
"Error loading configuration file '{}'. File could not be found",
config
));
}
}
else {
// user customized SGCT config
}
}
global::configuration->windowConfiguration = config;
}
return { config, isGeneratedConfig };
}
} // namespace
@@ -1325,9 +1262,6 @@ int main(int argc, char* argv[]) {
global::configuration->bypassLauncher = *commandlineArguments.bypassLauncher;
}
// Determining SGCT configuration file
LDEBUG("SGCT Configuration file: " + global::configuration->windowConfiguration);
windowConfiguration = global::configuration->windowConfiguration;
}
catch (const documentation::SpecificationError& e) {
@@ -1350,12 +1284,20 @@ int main(int argc, char* argv[]) {
// Call profile GUI
const std::string labelFromCfgFile = " (from .cfg)";
std::string windowCfgPreset = setWindowConfigPresetForGui(
labelFromCfgFile,
commandlineArguments.windowConfig.has_value()
);
std::string windowCfgPreset;
if (commandlineArguments.windowConfig.has_value()) {
windowCfgPreset = std::format(
"{} (from CLI)", global::configuration->windowConfiguration
);
}
else if (!global::configuration->sgctConfigNameInitialized.empty()) {
windowCfgPreset =
global::configuration->sgctConfigNameInitialized + labelFromCfgFile;
}
else {
windowCfgPreset = global::configuration->windowConfiguration;
}
//TODO consider LFATAL if ${USER} doens't exist rather then recurisve create.
global::openSpaceEngine->createUserDirectoriesIfNecessary();
// (abock, 2020-12-07) For some reason on Apple the keyboard handler in CEF will call
@@ -1387,17 +1329,16 @@ int main(int argc, char* argv[]) {
);
}
LauncherWindow win = LauncherWindow(
LauncherWindow launcher = LauncherWindow(
!commandlineArguments.profile.has_value(),
*global::configuration,
!commandlineArguments.windowConfig.has_value(),
std::move(windowCfgPreset),
nullptr
std::move(windowCfgPreset)
);
win.show();
launcher.show();
QApplication::exec();
if (!win.wasLaunchSelected()) {
if (!launcher.wasLaunchSelected()) {
exit(EXIT_SUCCESS);
}
glfwInit();
@@ -1442,14 +1383,23 @@ int main(int argc, char* argv[]) {
size
);
global::configuration->profile = win.selectedProfile();
std::tie(windowConfiguration, isGeneratedWindowConfig) =
selectedSgctProfileFromLauncher(
win,
commandlineArguments.windowConfig.has_value(),
windowConfiguration,
labelFromCfgFile
);
global::configuration->profile = launcher.selectedProfile();
std::string config = windowConfiguration;
isGeneratedWindowConfig = false;
if (!commandlineArguments.windowConfig.has_value()) {
config = launcher.selectedWindowConfig();
if (config.find(labelFromCfgFile) != std::string::npos) {
if (config.find("sgct.config") == std::string::npos) {
config = config.substr(0, config.length() - labelFromCfgFile.length());
}
else {
config = windowConfiguration;
isGeneratedWindowConfig = true;
}
}
global::configuration->windowConfiguration = config;
}
}
else {
glfwInit();
@@ -1478,7 +1428,10 @@ int main(int argc, char* argv[]) {
// as well as the configuration file that sgct is supposed to use
arguments.insert(arguments.begin(), argv[0]);
arguments.insert(arguments.begin() + 1, "-config");
arguments.insert(arguments.begin() + 2, absPath(windowConfiguration).string());
arguments.insert(
arguments.begin() + 2,
absPath(global::configuration->windowConfiguration).string()
);
// Need to set this before the creation of the sgct::Engine
@@ -1491,6 +1444,12 @@ int main(int argc, char* argv[]) {
glfwWindowHint(GLFW_STENCIL_BITS, 8);
#endif
// Determining SGCT configuration file
LINFO(std::format(
"SGCT Configuration file: {}", absPath(global::configuration->windowConfiguration)
));
LDEBUG("Creating SGCT Engine");
std::vector<std::string> arg(argv + 1, argv + argc);
LDEBUG("Parsing commandline arguments");
@@ -1498,7 +1457,9 @@ int main(int argc, char* argv[]) {
LDEBUG("Loading cluster information");
config::Cluster cluster;
try {
cluster = loadCluster(absPath(windowConfiguration).string());
cluster = loadCluster(
absPath(global::configuration->windowConfiguration).string()
);
}
catch (const std::runtime_error& e) {
LFATALC("main", e.what());

View File

@@ -1593,8 +1593,8 @@ void OpenSpaceEngine::handleDragDrop(std::filesystem::path file) {
// each of the files contained in the directory
std::vector<std::filesystem::path> files = ghoul::filesystem::walkDirectory(
file,
true,
false,
ghoul::filesystem::Recursive::Yes,
ghoul::filesystem::Sorted::No,
[](const std::filesystem::path& f) {
return std::filesystem::is_regular_file(f);
}

View File

@@ -162,8 +162,8 @@ namespace {
namespace fs = std::filesystem;
return ghoul::filesystem::walkDirectory(
path,
recursive,
sorted,
ghoul::filesystem::Recursive(recursive),
ghoul::filesystem::Sorted(sorted),
[](const fs::path& p) { return fs::is_directory(p) || fs::is_regular_file(p); }
);
}
@@ -187,8 +187,8 @@ namespace {
namespace fs = std::filesystem;
return ghoul::filesystem::walkDirectory(
path,
recursive,
sorted,
ghoul::filesystem::Recursive(recursive),
ghoul::filesystem::Sorted(sorted),
[](const fs::path& p) { return fs::is_regular_file(p); }
);
}
@@ -212,8 +212,8 @@ namespace {
namespace fs = std::filesystem;
return ghoul::filesystem::walkDirectory(
path,
recursive,
sorted,
ghoul::filesystem::Recursive(recursive),
ghoul::filesystem::Sorted(sorted),
[](const fs::path& p) { return fs::is_directory(p); }
);
}