From a209d0372a1a5ec587371351d2b7690cba065977 Mon Sep 17 00:00:00 2001
From: "Anthony J. Thibault" <tony@highfidelity.io>
Date: Wed, 8 Feb 2017 17:27:19 -0800
Subject: [PATCH] First cut of pal on tablet.

---
 interface/resources/qml/hifi/Pal.qml          |  6 +-
 .../resources/qml/hifi/tablet/TabletRoot.qml  | 13 ++++
 interface/src/ui/overlays/Web3DOverlay.cpp    |  7 ++
 libraries/gl/src/gl/OffscreenQmlSurface.cpp   | 12 ++++
 libraries/gl/src/gl/OffscreenQmlSurface.h     |  5 ++
 .../src/TabletScriptingInterface.cpp          | 23 +++++++
 .../src/TabletScriptingInterface.h            | 23 +++++++
 scripts/system/pal.js                         | 66 ++++++++++++++-----
 8 files changed, 138 insertions(+), 17 deletions(-)

diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml
index 923b09b9ef..20376d3fc0 100644
--- a/interface/resources/qml/hifi/Pal.qml
+++ b/interface/resources/qml/hifi/Pal.qml
@@ -16,6 +16,8 @@ import QtQuick.Controls 1.4
 import "../styles-uit"
 import "../controls-uit" as HifiControls
 
+// references HMD, Users, UserActivityLogger from root context
+
 Rectangle {
     id: pal
     // Size
@@ -35,7 +37,9 @@ Rectangle {
     // Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server.
     // NOTE: if another script modifies the per-avatar gain, this value won't be accurate!
     property var gainSliderValueDB: ({});
-    
+
+    HifiConstants { id: hifi }
+
     // The letterbox used for popup messages
     LetterboxMessage {
         id: letterboxMessage
diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml
index cfda92e774..0260bd6a01 100644
--- a/interface/resources/qml/hifi/tablet/TabletRoot.qml
+++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml
@@ -18,6 +18,16 @@ Item {
         loader.item.scriptURL = injectedJavaScriptUrl;
     }
 
+    // used to send a message from qml to interface script.
+    signal sendToScript(var message);
+
+    // used to receive messages from interface script
+    function fromScript(message) {
+        if (loader.item.hasOwnProperty("fromScript")) {
+            loader.item.fromScript(message);
+        }
+    }
+
     SoundEffect {
         id: buttonClickSound
         volume: 0.1
@@ -55,6 +65,9 @@ Item {
                     }
                 });
             }
+            if (loader.item.hasOwnProperty("sendToScript")) {
+                loader.item.sendToScript.connect(tabletRoot.sendToScript);
+            }
             loader.item.forceActiveFocus();
         }
     }
diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp
index f33ef24c0d..a381b90bfb 100644
--- a/interface/src/ui/overlays/Web3DOverlay.cpp
+++ b/interface/src/ui/overlays/Web3DOverlay.cpp
@@ -23,11 +23,14 @@
 #include <DependencyManager.h>
 #include <GeometryCache.h>
 #include <GeometryUtil.h>
+#include <scripting/HMDScriptingInterface.h>
 #include <gl/OffscreenQmlSurface.h>
 #include <PathUtils.h>
 #include <RegisteredMetaTypes.h>
 #include <TabletScriptingInterface.h>
 #include <TextureCache.h>
+#include <UsersScriptingInterface.h>
+#include <UserActivityLoggerScriptingInterface.h>
 #include <AbstractViewStateInterface.h>
 #include <gl/OffscreenQmlSurface.h>
 #include <gl/OffscreenQmlSurfaceCache.h>
@@ -149,6 +152,10 @@ void Web3DOverlay::loadSourceURL() {
         _webSurface->load(_url, [&](QQmlContext* context, QObject* obj) {});
         _webSurface->resume();
 
+        _webSurface->getRootContext()->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
+        _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
+        _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
+
         if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") {
             auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
             auto flags = tabletScriptingInterface->getFlags();
diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp
index 8af115ebcb..447b9d56aa 100644
--- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp
+++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp
@@ -604,6 +604,9 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
         qFatal("Could not load object as root item");
         return nullptr;
     }
+
+    connect(newItem, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(QVariant)));
+
     // The root item is ready. Associate it with the window.
     _rootItem = newItem;
     _rootItem->setParentItem(_quickWindow->contentItem());
@@ -952,4 +955,13 @@ void OffscreenQmlSurface::emitWebEvent(const QVariant& message) {
     }
 }
 
+void OffscreenQmlSurface::sendToQml(const QVariant& message) {
+    if (QThread::currentThread() != thread()) {
+        QMetaObject::invokeMethod(this, "emitQmlEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
+    } else if (_rootItem) {
+        // call fromScript method on qml root
+        QMetaObject::invokeMethod(_rootItem, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message));
+    }
+}
+
 #include "OffscreenQmlSurface.moc"
diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h
index f6168e7b6d..efd35fce8b 100644
--- a/libraries/gl/src/gl/OffscreenQmlSurface.h
+++ b/libraries/gl/src/gl/OffscreenQmlSurface.h
@@ -107,6 +107,11 @@ signals:
     void scriptEventReceived(const QVariant& message);
     void webEventReceived(const QVariant& message);
 
+    // qml event bridge
+public slots:
+    void sendToQml(const QVariant& message);
+signals:
+    void fromQml(QVariant message);
 
 protected:
     bool filterEnabled(QObject* originalDestination, QEvent* event) const;
diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp
index e7f8ebe2cb..616d751bf7 100644
--- a/libraries/script-engine/src/TabletScriptingInterface.cpp
+++ b/libraries/script-engine/src/TabletScriptingInterface.cpp
@@ -183,6 +183,18 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr
     _qmlTabletRoot = qmlTabletRoot;
     if (_qmlTabletRoot && _qmlOffscreenSurface) {
         QObject::connect(_qmlOffscreenSurface, SIGNAL(webEventReceived(QVariant)), this, SIGNAL(webEventReceived(QVariant)));
+
+        // forward qml surface events to interface js
+        connect(dynamic_cast<OffscreenQmlSurface*>(_qmlOffscreenSurface), &OffscreenQmlSurface::fromQml, [this](QVariant message) {
+            if (message.canConvert<QJSValue>()) {
+                emit fromQml(qvariant_cast<QJSValue>(message).toVariant());
+            } else if (message.canConvert<QString>()) {
+                emit fromQml(message.toString());
+            } else {
+                qWarning() << "fromQml: Unsupported message type " << message;
+            }
+        });
+
         gotoHomeScreen();
 
         QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername())));
@@ -197,6 +209,7 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr
     } else {
         removeButtonsFromHomeScreen();
         _state = State::Uninitialized;
+        emit screenChanged(QVariant("Closed"), QVariant(""));
     }
 }
 
@@ -208,6 +221,7 @@ void TabletProxy::gotoMenuScreen() {
             QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen()), Qt::DirectConnection);
             QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL)));
             _state = State::Menu;
+            emit screenChanged(QVariant("Menu"), QVariant(VRMENU_SOURCE_URL));
         }
     }
 }
@@ -217,6 +231,7 @@ void TabletProxy::loadQMLSource(const QVariant& path) {
         if (_state != State::QML) {
             QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, path));
             _state = State::QML;
+            emit screenChanged(QVariant("QML"), path);
         }
     }
 }
@@ -228,6 +243,7 @@ void TabletProxy::gotoHomeScreen() {
             QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL)));
             QMetaObject::invokeMethod(_qmlTabletRoot, "playButtonClickSound");
             _state = State::Home;
+            emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL));
         }
     }
 }
@@ -244,6 +260,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS
         if (_state != State::Web) {
             QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL)));
             _state = State::Web;
+            emit screenChanged(QVariant("Web"), QVariant(url));
         }
         QMetaObject::invokeMethod(_qmlTabletRoot, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)),
                                   Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl)));
@@ -306,6 +323,12 @@ void TabletProxy::emitScriptEvent(QVariant msg) {
     }
 }
 
+void TabletProxy::sendToQml(QVariant msg) {
+    if (_qmlOffscreenSurface) {
+        QMetaObject::invokeMethod(_qmlOffscreenSurface, "sendToQml", Qt::AutoConnection, Q_ARG(QVariant, msg));
+    }
+}
+
 void TabletProxy::addButtonsToHomeScreen() {
     auto tablet = getQmlTablet();
     if (!tablet) {
diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h
index a005152fa9..93f5bcf6ba 100644
--- a/libraries/script-engine/src/TabletScriptingInterface.h
+++ b/libraries/script-engine/src/TabletScriptingInterface.h
@@ -122,6 +122,13 @@ public:
      */
     Q_INVOKABLE void emitScriptEvent(QVariant msg);
 
+    /**jsdoc
+     * Used to send an event to the qml embedded in the tablet
+     * @function TabletProxy#sendToQml
+     * @param msg {object|string}
+     */
+    Q_INVOKABLE void sendToQml(QVariant msg);
+
     Q_INVOKABLE bool onHomeScreen();
 
     QObject* getTabletSurface();
@@ -139,6 +146,22 @@ signals:
      */
     void webEventReceived(QVariant msg);
 
+    /**jsdoc
+     * Signaled when this tablet receives an event from the qml embedded in the tablet
+     * @function TabletProxy#fromQml
+     * @param msg {object|string}
+     * @returns {Signal}
+     */
+    void fromQml(QVariant msg);
+
+    /**jsdoc
+     * Signales when this tablet screen changes.
+     * @function TabletProxy#screenChanged
+     * @param type {string} - "Home", "Web", "Menu", "QML", "Closed"
+     * @param url {string} - only valid for Web and QML.
+     */
+    void screenChanged(QVariant type, QVariant url);
+
 private slots:
     void addButtonsToHomeScreen();
     void addButtonsToMenuScreen();
diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index adbde0ef5c..5f02be83e5 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -203,8 +203,8 @@ var pal = new OverlayWindow({
     height: 640,
     visible: false
 });
-pal.fromQml.connect(function (message) { // messages are {method, params}, like json-rpc. See also sendToQml.
-    print('From PAL QML:', JSON.stringify(message));
+function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
+    print('AJT: From PAL QML:', JSON.stringify(message));
     switch (message.method) {
     case 'selected':
         selectedIds = message.params;
@@ -234,6 +234,7 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like
         }
         break;
     case 'refresh':
+        print("AJT: REFRESH!");
         removeOverlays();
         populateUserList(message.params);
         UserActivityLogger.palAction("refresh", "");
@@ -259,7 +260,15 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like
     default:
         print('Unrecognized message from Pal.qml:', JSON.stringify(message));
     }
-});
+}
+
+function sendToQml(message) {
+    if (Settings.getValue("HUDUIEnabled")) {
+        pal.sendToQml(message);
+    } else {
+        tablet.sendToQml(message);
+    }
+}
 
 //
 // Main operations.
@@ -298,10 +307,10 @@ function populateUserList(selectData) {
         data.push(avatarPalDatum);
         print('PAL data:', JSON.stringify(avatarPalDatum));
     });
-    pal.sendToQml({ method: 'users', params: data });
+    sendToQml({ method: 'users', params: data });
     if (selectData) {
         selectData[2] = true;
-        pal.sendToQml({ method: 'select', params: selectData });
+        sendToQml({ method: 'select', params: selectData });
     }
 }
 
@@ -322,7 +331,7 @@ function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
     }
     print('Username Data:', JSON.stringify(data));
     // Ship the data off to QML
-    pal.sendToQml({ method: 'updateUsername', params: data });
+    sendToQml({ method: 'updateUsername', params: data });
 }
 
 var pingPong = true;
@@ -396,7 +405,7 @@ function handleClick(pickRay) {
     ExtendedOverlay.applyPickRay(pickRay, function (overlay) {
         // Don't select directly. Tell qml, who will give us back a list of ids.
         var message = {method: 'select', params: [[overlay.key], !overlay.selected, false]};
-        pal.sendToQml(message);
+        sendToQml(message);
         return true;
     });
 }
@@ -492,6 +501,7 @@ if (Settings.getValue("HUDUIEnabled")) {
         visible: true,
         alpha: 0.9
     });
+    pal.fromQml.connect(fromQml);
 } else {
     tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
     button = tablet.addButton({
@@ -499,7 +509,9 @@ if (Settings.getValue("HUDUIEnabled")) {
         icon: "icons/tablet-icons/people-i.svg",
         sortOrder: 7
     });
+    tablet.fromQml.connect(fromQml);
 }
+
 var isWired = false;
 var audioTimer;
 var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too)
@@ -518,10 +530,26 @@ function off() {
     Users.requestsDomainListData = false;
 }
 function onClicked() {
-    if (!pal.visible) {
+    if (Settings.getValue("HUDUIEnabled")) {
+        if (!pal.visible) {
+            Users.requestsDomainListData = true;
+            populateUserList();
+            pal.raise();
+            isWired = true;
+            Script.update.connect(updateOverlays);
+            Controller.mousePressEvent.connect(handleMouseEvent);
+            Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
+            triggerMapping.enable();
+            triggerPressMapping.enable();
+            audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS);
+        } else {
+            off();
+        }
+        pal.setVisible(!pal.visible);
+    } else {
+        tablet.loadQMLSource("../Pal.qml");
         Users.requestsDomainListData = true;
         populateUserList();
-        pal.raise();
         isWired = true;
         Script.update.connect(updateOverlays);
         Controller.mousePressEvent.connect(handleMouseEvent);
@@ -529,10 +557,7 @@ function onClicked() {
         triggerMapping.enable();
         triggerPressMapping.enable();
         audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS);
-    } else {
-        off();
     }
-    pal.setVisible(!pal.visible);
 }
 
 //
@@ -550,7 +575,7 @@ function receiveMessage(channel, messageString, senderID) {
         if (!pal.visible) {
             onClicked();
         }
-        pal.sendToQml(message); // Accepts objects, not just strings.
+        sendToQml(message); // Accepts objects, not just strings.
         break;
     default:
         print('Unrecognized PAL message', messageString);
@@ -607,13 +632,13 @@ function createAudioInterval(interval) {
             var userId = id || 0;
             param[userId] = level;
         });
-        pal.sendToQml({method: 'updateAudioLevel', params: param});
+        sendToQml({method: 'updateAudioLevel', params: param});
     }, interval);
 }
 
 function avatarDisconnected(nodeID) {
     // remove from the pal list
-    pal.sendToQml({method: 'avatarDisconnected', params: [nodeID]});
+    sendToQml({method: 'avatarDisconnected', params: [nodeID]});
 }
 //
 // Button state.
@@ -624,11 +649,20 @@ function onVisibleChanged() {
 button.clicked.connect(onClicked);
 pal.visibleChanged.connect(onVisibleChanged);
 pal.closed.connect(off);
+
+if (!Settings.getValue("HUDUIEnabled")) {
+    tablet.screenChanged.connect(function (type, url) {
+        if (type !== "QML" || url !== "../Pal.qml") {
+            off();
+        }
+    });
+}
+
 Users.usernameFromIDReply.connect(usernameFromIDReply);
 Users.avatarDisconnected.connect(avatarDisconnected);
 
 function clearLocalQMLDataAndClosePAL() {
-    pal.sendToQml({ method: 'clearLocalQMLData' });
+    sendToQml({ method: 'clearLocalQMLData' });
     if (pal.visible) {
         onClicked(); // Close the PAL
     }