From 8884988336d047992f484ed2d194e881e7b22560 Mon Sep 17 00:00:00 2001
From: Dale Glass <dale@daleglass.net>
Date: Fri, 6 Jan 2023 17:31:34 +0100
Subject: [PATCH] Add an option to choose which screen to use for full screen
 mode

---
 .../dialogs/graphics/GraphicsSettings.qml     | 124 ++++++++++++++++--
 .../scripting/RenderScriptingInterface.cpp    |  46 +++++++
 .../src/scripting/RenderScriptingInterface.h  |  35 ++++-
 .../Basic2DWindowOpenGLDisplayPlugin.cpp      |  12 +-
 4 files changed, 200 insertions(+), 17 deletions(-)

diff --git a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
index 6235747a1c..c1901d0d79 100644
--- a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
+++ b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
@@ -155,7 +155,7 @@ Item {
                             text: "Full World Detail"
                         }
                     }
-                
+
                     HifiControlsUit.ComboBox {
                         id: worldDetailDropdown
                         enabled: performanceCustom.checked
@@ -175,7 +175,7 @@ Item {
                         Component.onCompleted: {
                             worldDetailDropdown.refreshWorldDetailDropdown();
                         }
-                        
+
                         onCurrentIndexChanged: {
                             LODManager.worldDetailQuality = currentIndex;
                             worldDetailDropdown.displayText = model.get(currentIndex).text;
@@ -337,7 +337,7 @@ Item {
                             refreshRatePreset: 2 // RefreshRateProfile::REALTIME
                         }
                     }
-                
+
                     HifiControlsUit.ComboBox {
                         id: refreshRateDropdown
                         enabled: performanceCustom.checked
@@ -363,7 +363,7 @@ Item {
                         Component.onCompleted: {
                             refreshRateDropdown.refreshRefreshRateDropdownDisplay();
                         }
-                        
+
                         onCurrentIndexChanged: {
                             Performance.setRefreshRateProfile(model.get(currentIndex).refreshRatePreset);
                             refreshRateDropdown.displayText = model.get(currentIndex).text;
@@ -386,14 +386,14 @@ Item {
                         size: 16
                         color: "#FFFFFF"
                     }
-                
+
                     HifiControlsUit.Slider {
                         id: resolutionScaleSlider
                         enabled: performanceCustom.checked
                         anchors.left: resolutionHeader.right
                         anchors.leftMargin: 57
                         anchors.top: parent.top
-                        width: 150 
+                        width: 150
                         height: parent.height
                         colorScheme: hifi.colorSchemes.dark
                         minimumValue: 0.25
@@ -424,11 +424,11 @@ Item {
                 Layout.topMargin: 20
                 Layout.preferredWidth: parent.width
                 spacing: 0
-    
+
                 Item {
                     Layout.preferredWidth: parent.width
                     Layout.preferredHeight: 35
-    
+
                     HifiStylesUit.RalewayRegular {
                         id: antialiasingHeader
                         text: "Anti-aliasing"
@@ -454,7 +454,7 @@ Item {
                             text: "FXAA"
                         }
                     }
-    
+
                     HifiControlsUit.ComboBox {
                         id: antialiasingDropdown
                         anchors.left: antialiasingHeader.right
@@ -465,15 +465,15 @@ Item {
                         colorScheme: hifi.colorSchemes.dark
                         model: antialiasingModel
                         currentIndex: -1
-    
+
                         function refreshAntialiasingDropdown() {
                             antialiasingDropdown.currentIndex = Render.antialiasingMode;
                         }
-    
+
                         Component.onCompleted: {
                             antialiasingDropdown.refreshAntialiasingDropdown();
                         }
-    
+
                         onCurrentIndexChanged: {
                             Render.antialiasingMode = currentIndex;
                             antialiasingDropdown.displayText = model.get(currentIndex).text;
@@ -481,6 +481,106 @@ Item {
                     }
                 }
             }
+
+            ColumnLayout {
+                Layout.topMargin: 20
+                Layout.preferredWidth: parent.width
+                spacing: 0
+
+                Item {
+                    Layout.preferredWidth: parent.width
+                    Layout.preferredHeight: 35
+
+                    HifiStylesUit.RalewayRegular {
+                        id: fullScreenDisplayHeader
+                        text: "Full screen display"
+                        anchors.left: parent.left
+                        anchors.top: parent.top
+                        width: 130
+                        height: parent.height
+                        size: 16
+                        color: "#FFFFFF"
+                    }
+
+                    ListModel {
+                        id: fullScreenDisplayModel
+
+                        ListElement {
+                            text: "Screen 1"
+                        }
+
+                        function refreshScreens() {
+                            fullScreenDisplayModel.clear();
+                            Render.getScreens().forEach(function(screen) {
+                                fullScreenDisplayModel.append({"text" : screen});
+                            });
+                        }
+
+                        Component.onCompleted: {
+                            fullScreenDisplayModel.refreshScreens();
+                        }
+                    }
+
+                    HifiControlsUit.ComboBox {
+                        id: fullScreenDisplayDropdown
+                        anchors.left: fullScreenDisplayHeader.right
+                        anchors.leftMargin: 20
+                        anchors.top: parent.top
+                        width: 280
+                        height: parent.height
+                        colorScheme: hifi.colorSchemes.dark
+                        model: fullScreenDisplayModel
+                        currentIndex: 0
+
+                        function refreshFullScreenDisplayDropdown() {
+                            var screens = Render.getScreens();
+                            var selected = Render.getFullScreenScreen();
+
+                            for(let idx = 0; idx < screens.length; idx++) {
+                                if (screens[idx] == selected) {
+                                    fullScreenDisplayDropdown.currentIndex = idx;
+                                    return;
+                                }
+                            }
+
+                            console.log("Selected full screen screen", selected, "not found, falling back to primary screen");
+                            console.log("List of screens is:", screens);
+                            fullScreenDisplayDropdown.currentIndex = 0;
+                        }
+
+                        Component.onCompleted: {
+                            model.refreshScreens();
+                            fullScreenDisplayDropdown.refreshFullScreenDisplayDropdown();
+                            fullScreenDisplayDropdown.displayText = model.get(currentIndex).text;
+                        }
+
+                        onCurrentIndexChanged: {
+                            if (currentIndex >= 0) {
+                                // Somehow, we end up going through here twice on every change of the combo box.
+                                // The first one is with the new selected index, and the second one is with the
+                                // index at -1.
+                                //
+                                // The first one comes from a sensible stack of:
+                                //     onCurrentIndexChanged (qrc:/qml/hifi/dialogs/graphics/GraphicsSettings.qml:559)
+                                //     refreshScreens (qrc:/qml/hifi/dialogs/graphics/GraphicsSettings.qml:514)
+                                //     onCompleted (qrc:/qml/hifi/dialogs/graphics/GraphicsSettings.qml:553)
+                                //     load (qrc:/qml/hifi/tablet/WindowRoot.qml:170)
+                                //     loadSource (qrc:/qml/hifi/tablet/WindowRoot.qml:63)
+                                //
+                                // The second seems to be called out of nowhere. This likely indicates some sort of bug.
+                                // Might be related to Wayland?
+
+                                Render.setFullScreenScreen(model.get(currentIndex).text);
+                                fullScreenDisplayDropdown.displayText = model.get(currentIndex).text;
+                            } else {
+                                console.log("Called with currentIndex =", currentIndex);
+                                console.trace();
+                            }
+                        }
+                    }
+                }
+            }
+
         }
     }
 
diff --git a/interface/src/scripting/RenderScriptingInterface.cpp b/interface/src/scripting/RenderScriptingInterface.cpp
index de56ffb321..515aeb8c29 100644
--- a/interface/src/scripting/RenderScriptingInterface.cpp
+++ b/interface/src/scripting/RenderScriptingInterface.cpp
@@ -8,6 +8,7 @@
 #include "RenderScriptingInterface.h"
 
 #include "LightingModel.h"
+#include <QScreen>
 
 
 RenderScriptingInterface* RenderScriptingInterface::getInstance() {
@@ -31,7 +32,17 @@ void RenderScriptingInterface::loadSettings() {
         //_antialiasingMode = (_antialiasingModeSetting.get());
         _antialiasingMode = static_cast<AntialiasingConfig::Mode>(_antialiasingModeSetting.get());
         _viewportResolutionScale = (_viewportResolutionScaleSetting.get());
+        _fullScreenScreen = (_fullScreenScreenSetting.get());
     });
+
+    // If full screen screen is not initialized, or set to an invalid value,
+    // set to the first screen.
+    auto screens = getScreens();
+    if (std::find(screens.begin(), screens.end(), _fullScreenScreen) == screens.end()) {
+        setFullScreenScreen(screens.first());
+    }
+
+
     forceRenderMethod((RenderMethod)_renderMethod);
     forceShadowsEnabled(_shadowsEnabled);
     forceAmbientOcclusionEnabled(_ambientOcclusionEnabled);
@@ -193,6 +204,41 @@ void RenderScriptingInterface::setViewportResolutionScale(float scale) {
     }
 }
 
+QStringList RenderScriptingInterface::getScreens() const {
+    QStringList screens;
+
+    for(QScreen *screen : qApp->screens()) {
+        screens << screen->model();
+    }
+
+    return screens;
+}
+
+bool RenderScriptingInterface::setFullScreenScreen(QString name) {
+    auto screens = getScreens();
+
+    if (std::find(screens.begin(), screens.end(), name) == screens.end()) {
+        // Screens can come and go and don't have a stable opaque ID, so we
+        // go by model here. For multiple screens with the same model we get names
+        // that include a serial number, so it works.
+        return false;
+    }
+
+    _renderSettingLock.withWriteLock([&] {
+        _fullScreenScreen = name;
+        _fullScreenScreenSetting.set(name);
+    });
+
+    emit settingsChanged();
+
+    return true;
+}
+
+QString RenderScriptingInterface::getFullScreenScreen() const {
+    return _fullScreenScreen;
+}
+
+
 void RenderScriptingInterface::forceViewportResolutionScale(float scale) {
     // just not negative values or zero
     if (scale <= 0.f) {
diff --git a/interface/src/scripting/RenderScriptingInterface.h b/interface/src/scripting/RenderScriptingInterface.h
index 62c50b10f1..f107ae06d4 100644
--- a/interface/src/scripting/RenderScriptingInterface.h
+++ b/interface/src/scripting/RenderScriptingInterface.h
@@ -27,7 +27,7 @@
  *
  * @property {Render.RenderMethod} renderMethod - The render method being used.
  * @property {boolean} shadowsEnabled - <code>true</code> if shadows are enabled, <code>false</code> if they're disabled.
- * @property {boolean} ambientOcclusionEnabled - <code>true</code> if ambient occlusion is enabled, <code>false</code> if it's 
+ * @property {boolean} ambientOcclusionEnabled - <code>true</code> if ambient occlusion is enabled, <code>false</code> if it's
  *     disabled.
  * @property {integer} antialiasingMode - The active anti-aliasing mode.
  * @property {number} viewportResolutionScale - The view port resolution scale, <code>&gt; 0.0</code>.
@@ -52,9 +52,9 @@ public:
      *     <tr><th>Value</th><th>Name</th><th>Description</th>
      *   </thead>
      *   <tbody>
-     *     <tr><td><code>0</code></td><td>DEFERRED</td><td>More complex rendering pipeline where lighting is applied to the 
+     *     <tr><td><code>0</code></td><td>DEFERRED</td><td>More complex rendering pipeline where lighting is applied to the
      *       scene as a whole after all objects have been rendered.</td></tr>
-     *     <tr><td><code>1</code></td><td>FORWARD</td><td>Simpler rendering pipeline where each object in the scene, in turn, 
+     *     <tr><td><code>1</code></td><td>FORWARD</td><td>Simpler rendering pipeline where each object in the scene, in turn,
      *       is rendered and has lighting applied.</td></tr>
      *   </tbody>
      * </table>
@@ -172,8 +172,32 @@ public slots:
      */
     void setViewportResolutionScale(float resolutionScale);
 
+    /*@jsdoc
+     * Returns the list of screens
+     * @function Render.getScreens
+     * @returns {string[]} The names of the available screens
+     */
+    QStringList getScreens() const;
+
+    /*@jsdoc
+     * Gets the screen used when switching to full screen mode
+     * @function Render.getFullScreenScreen
+     * @returns {string} The name of the screen used for full screen mode
+     */
+    QString getFullScreenScreen() const;
+
+    /*@jsdoc
+     * Sets the screen used when switching to full screen mode
+     * This function will only succeed if the name passed is one of the entries from Render.getScreens.
+     * Otherwise, it will return False and have no effect.
+     *
+     * @function Render.setFullScreenScreen
+     * @returns {bool} True if the setting was successful
+     */
+    bool setFullScreenScreen(QString name);
+
 signals:
-    
+
     /*@jsdoc
      * Triggered when one of the <code>Render</code> API's properties changes.
      * @function Render.settingsChanged
@@ -196,6 +220,8 @@ private:
     bool _ambientOcclusionEnabled{ false };
     AntialiasingConfig::Mode _antialiasingMode{ AntialiasingConfig::Mode::NONE };
     float _viewportResolutionScale{ 1.0f };
+    QString _fullScreenScreen;
+
 
     // Actual settings saved on disk
     Setting::Handle<int> _renderMethodSetting { "renderMethod", RENDER_FORWARD ? render::Args::RenderMethod::FORWARD : render::Args::RenderMethod::DEFERRED };
@@ -204,6 +230,7 @@ private:
     //Setting::Handle<AntialiasingConfig::Mode> _antialiasingModeSetting { "antialiasingMode", AntialiasingConfig::Mode::TAA };
     Setting::Handle<int> _antialiasingModeSetting { "antialiasingMode", AntialiasingConfig::Mode::NONE };
     Setting::Handle<float> _viewportResolutionScaleSetting { "viewportResolutionScale", 1.0f };
+    Setting::Handle<QString> _fullScreenScreenSetting { "fullScreenScreen", "" };
 
     // Force assign both setting AND runtime value to the parameter value
     void forceRenderMethod(RenderMethod renderMethod);
diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp
index a35496f88e..a278f0cad9 100644
--- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp
+++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp
@@ -18,6 +18,7 @@
 
 #include <ui-plugins/PluginContainer.h>
 #include <PathUtils.h>
+#include "SettingHandle.h"
 
 const QString Basic2DWindowOpenGLDisplayPlugin::NAME("Desktop");
 
@@ -165,8 +166,17 @@ bool Basic2DWindowOpenGLDisplayPlugin::isThrottled() const {
     return _isThrottled;
 }
 
-// FIXME target the screen the window is currently on
 QScreen* Basic2DWindowOpenGLDisplayPlugin::getFullscreenTarget() {
+    Setting::Handle<QString> _fullScreenScreenSetting { "fullScreenScreen", "" };
+    QString selectedModel = _fullScreenScreenSetting.get();
+
+    for(QScreen *screen : qApp->screens()) {
+        if (screen->model() == selectedModel) {
+            return screen;
+        }
+    }
+
+    qWarning() << "Failed to find selected screen" << selectedModel << "for full screen mode, using primary screen";
     return qApp->primaryScreen();
 }