diff --git a/applications/metadata.js b/applications/metadata.js
index 055103a..cb14853 100644
--- a/applications/metadata.js
+++ b/applications/metadata.js
@@ -152,6 +152,15 @@ var metadata = { "applications":
"jsfile": "home/app_home.js",
"icon": "home/appicon_i.png",
"caption": "HOME"
+ },
+ {
+ "isActive": true,
+ "directory": "tabletCam",
+ "name": "Camera Snap-Pro",
+ "description": "The Camera 'Snap-Pro' allows you to take high quality in-world photos and selfies (Low, Normal, 4K, and 'EXTREME' resolution). It has two cameras on the tablet, front-facing/rear-facing, and one detachable to allow more flexibility. It supports different aspect ratio formats: 8x10, 2x3, 9x16 and 'Square'. It comes with an Optical Zoom, a trigger on the VR hand controller and other features.",
+ "jsfile": "tabletCam/tabletCam_app.js",
+ "icon": "tabletCam/appIcons/snap-pro-i.svg",
+ "caption": "SNAP-PRO"
}
]
};
diff --git a/applications/tabletCam/README.md b/applications/tabletCam/README.md
new file mode 100644
index 0000000..97a1601
--- /dev/null
+++ b/applications/tabletCam/README.md
@@ -0,0 +1,77 @@
+# Tablet Cam
+
+## Note
+
+This was a personal project by [Zach Fox](https://github.com/zfox23/), a member of the High Fidelity Experiences Team. It has not undergone the same code review or QA process as the rest of the projects in this repository.
+
+## Description
+
+The Tablet Cam app allows you to **easily take selfies and regular photos in High Fidelity/Overte** using your Tablet or hand controllers!
+
+## Features
+
+- **Front-facing and rear-facing** cameras and flashes with optional **custom positioning**
+- Works in **Desktop Mode and in VR Mode**
+- **Persistent "Camera Roll"** for reviewing photos that you recently took
+- Optical **zoom**
+- Photo **quality settings**: Low, Normal, 4k, and _EXTREME_!
+- **Aspect ratio settings**: 8x10, 2x3, 9x16, Square
+- Editable photo directory
+
+## Changelog
+
+### v2.4 (2022-08-22) by Alezia Kurdis
+
+- Fixed a but in HMD where the rear-facing camera was pointed to the tablet instead of the scene.
+- Replaced the camera model by a new one (nicer and less heavy, 198k instead of 576k, less but more optimal polygons, 1 PBR material.)
+- Renamed the tool: Camera Snap-Pro
+- Replaced the icons for something more easy to identify as a device to take picture ("camera"), as an advance version of the "Snap" app.
+- Add a notice in the settings page to informe the user about the trigger on the thumbstick of the right VR hand controller.
+
+### v2.3 (2019-10-15)
+
+- Removed unnecessary tone curve correction present in captured Tablet Cam output image.
+- Adjusted y-offset of Desktop-mode selfie cam to better serve Virtual You avatars.
+- Added `"Head"` as a backup joint to `"HeadTop_End"` for certain operations.
+
+### v2.2 (2019-07-09)
+
+- Fixed a hack in which the secondary camera feed was darkened to compensate for it being rendered too light.
+- If your camera looks too light, it means you have an older version of Interface (pre PR #15862) and should go back to the previous version of this script.
+- If your camera looks too dark, it means you have a newer version of Interface (post PR #15682) and should update to the current version of this script.
+
+### v2.1 (2019-06-27)
+
+- We've moved the code for this project into a new remote folder, which necessitated a version bump. There are no changes to functionality.
+
+### v2.0 (2019-04)
+
+**I've made some huge changes in v2.0!**
+
+- In Desktop mode, when using the rear-facing camera and while you're using Third Person Camera, Tablet Cam will now be parented to Interface's Third Person Camera!
+- The camera's viewpoint can now be detached from its default position! Snap a photo from a unique viewpoint.
+- Fixed a bug that caused zoom settings to be saved incorrectly between restarts.
+- Fixed a bug that caused the camera preview to show up as a corrupted image in HMD mode.
+- Fixed a bug that caused the position of the rear-facing camera to be incorrect in HMD mode.
+- Switched Overlay usage to Local Entities.
+- Now you can use Tablet Cam in Desktop mode while "Desktop Tablet becomes Toolbar" is unchecked - although I'm not sure why you'd want to do that. :)
+- Fixed some interface bugs.
+
+### v1.1 (2019-02)
+
+- Fixed a bug that caused Tablet Cam to erroneously appear on the Tablet after switching domains when it was previously active.
+
+### Tablet Cam v1.0 (2019-01)
+
+- Tablet Cam v1.0 is an update to Selfie Cam v1.0. It is a complete overhaul of the app. All of its features are new!
+
+### Selfie Cam v1.0 (2018-12)
+
+- Initial Release!
+
+## Attributions
+- "snap.wav" from "Camera Shutter, Fast, A.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
+- "switchCams.svg" from "rotate camera" by Diego Naive from the Noun Project
+- "orientation.svg" from "orientation" by Atif Arshad from the Noun Project
+- "camera.fbx" from "Digital camera" by Nick Ladd: https://poly.google.com/view/4A3SYVh_smq
+- "camera-a.svg" and "camera-i.svg" from "Selfie" by Path Lord from the Noun Project
\ No newline at end of file
diff --git a/applications/tabletCam/appIcons/snap-pro-a.svg b/applications/tabletCam/appIcons/snap-pro-a.svg
new file mode 100644
index 0000000..69f9dd8
--- /dev/null
+++ b/applications/tabletCam/appIcons/snap-pro-a.svg
@@ -0,0 +1,226 @@
+
+
+
+
diff --git a/applications/tabletCam/appIcons/snap-pro-i.svg b/applications/tabletCam/appIcons/snap-pro-i.svg
new file mode 100644
index 0000000..9559317
--- /dev/null
+++ b/applications/tabletCam/appIcons/snap-pro-i.svg
@@ -0,0 +1,226 @@
+
+
+
+
diff --git a/applications/tabletCam/models/camera.fbx b/applications/tabletCam/models/camera.fbx
new file mode 100644
index 0000000..5c63092
Binary files /dev/null and b/applications/tabletCam/models/camera.fbx differ
diff --git a/applications/tabletCam/modules/appUi.js b/applications/tabletCam/modules/appUi.js
new file mode 100644
index 0000000..5b2904a
--- /dev/null
+++ b/applications/tabletCam/modules/appUi.js
@@ -0,0 +1,388 @@
+"use strict";
+/* global Tablet, Script */
+//
+// libraries/appUi.js
+//
+// Created by Howard Stearns on 3/20/18.
+// Modified by Zach Fox on 2019-04-13
+// Copyright 2018 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+function AppUi(properties) {
+ var request = Script.require('request').request;
+ /* Example development order:
+ 1. var AppUi = Script.require('appUi');
+ 2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3).
+ 3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"});
+ (And if converting an existing app,
+ define var tablet = ui.tablet, button = ui.button; as needed.
+ remove button.clicked.[dis]connect and tablet.remove(button).)
+ 4. Define onOpened and onClosed behavior in #3, if any.
+ (And if converting an existing app, remove screenChanged.[dis]connect.)
+ 5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you
+ want a handler to be "always on", connect it yourself at script startup.
+ (And if converting an existing app, remove code that [un]wires that message handling such as
+ fromQml/sendToQml or webEventReceived/emitScriptEvent.)
+ 6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet,
+ and use isOpen, open(), and close() as needed.)
+ 7. lint!
+ */
+ var that = this;
+ function defaultButton(name, suffix) {
+ var base = that[name] || (that.buttonPrefix + suffix);
+ that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); // poor man's merge
+ }
+
+ // Defaults:
+ that.tabletName = "com.highfidelity.interface.tablet.system";
+ that.inject = "";
+ that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below.
+ that.additionalAppScreens = [];
+ that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen.
+ // Actual url may have prefix or suffix.
+ return that.currentVisibleUrl &&
+ ((that.home.indexOf(that.currentVisibleUrl) > -1) ||
+ (that.additionalAppScreens.indexOf(that.currentVisibleUrl) > -1));
+ };
+ that.setCurrentVisibleScreenMetadata = function setCurrentVisibleScreenMetadata(type, url) {
+ that.currentVisibleScreenType = type;
+ that.currentVisibleUrl = url;
+ };
+ that.open = function open(optionalUrl, optionalInject) { // How to open the app.
+ var url = optionalUrl || that.home;
+ var inject = optionalInject || that.inject;
+
+ if (that.isQMLUrl(url)) {
+ that.tablet.loadQMLSource(url);
+ } else {
+ that.tablet.gotoWebScreen(url, inject);
+ }
+ };
+ // Opens some app on top of the current app (on desktop, opens new window)
+ that.openNewAppOnTop = function openNewAppOnTop(url, optionalInject) {
+ var inject = optionalInject || "";
+ if (that.isQMLUrl(url)) {
+ that.tablet.loadQMLOnTop(url);
+ } else {
+ that.tablet.loadWebScreenOnTop(url, inject);
+ }
+ };
+ that.close = function close() { // How to close the app.
+ that.currentVisibleUrl = "";
+ // for toolbar-mode: go back to home screen, this will close the window.
+ that.tablet.gotoHomeScreen();
+ };
+ that.buttonActive = function buttonActive(isActive) { // How to make the button active (white).
+ that.button.editProperties({isActive: isActive});
+ };
+ that.isQMLUrl = function isQMLUrl(url) {
+ var type = /.qml$/.test(url) ? 'QML' : 'Web';
+ return type === 'QML';
+ };
+ that.isCurrentlyOnQMLScreen = function isCurrentlyOnQMLScreen() {
+ return that.currentVisibleScreenType === 'QML';
+ };
+
+ //
+ // START Notification Handling Defaults
+ //
+ that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button.
+ // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true.
+ that.button.editProperties({
+ icon: isWaiting ? that.normalMessagesButton : that.normalButton,
+ activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton
+ });
+ };
+ that.notificationPollTimeout = [false];
+ that.notificationPollTimeoutMs = [60000];
+ that.notificationPollEndpoint = [false];
+ that.notificationPollStopPaginatingConditionMet = [false];
+ that.notificationDataProcessPage = function (data) {
+ return data;
+ };
+ that.notificationPollCallback = [that.ignore];
+ that.notificationPollCaresAboutSince = [false];
+ that.notificationInitialCallbackMade = [false];
+ that.notificationDisplayBanner = function (message) {
+ if (!that.isOpen) {
+ Window.displayAnnouncement(message);
+ }
+ };
+ //
+ // END Notification Handling Defaults
+ //
+
+ // Handlers
+ that.onScreenChanged = function onScreenChanged(type, url) {
+ // Set isOpen, wireEventBridge, set buttonActive as appropriate,
+ // and finally call onOpened() or onClosed() IFF defined.
+ that.setCurrentVisibleScreenMetadata(type, url);
+
+ if (that.checkIsOpen(type, url)) {
+ that.wireEventBridge(true);
+ if (!that.isOpen) {
+ if (that.onOpened) {
+ that.onOpened();
+ }
+ that.buttonActive(true);
+ that.isOpen = true;
+ }
+ } else {
+ // A different screen is now visible, or the tablet has been closed.
+ // Tablet visibility is controlled separately by `tabletShownChanged()`
+ that.wireEventBridge(false);
+ if (that.isOpen) {
+ if (that.onClosed) {
+ that.onClosed();
+ }
+ that.buttonActive(false);
+ that.isOpen = false;
+ }
+ }
+ };
+
+ // Overwrite with the given properties:
+ Object.keys(properties).forEach(function (key) {
+ that[key] = properties[key];
+ });
+
+ //
+ // START Notification Handling
+ //
+
+ var currentDataPageToRetrieve = [];
+ var concatenatedServerResponse = [];
+ for (var i = 0; i < that.notificationPollEndpoint.length; i++) {
+ currentDataPageToRetrieve[i] = 1;
+ concatenatedServerResponse[i] = new Array();
+ }
+
+ var MAX_LOG_LENGTH_CHARACTERS = 300;
+ function requestCallback(error, response, optionalParams) {
+ var indexOfRequest = optionalParams.indexOfRequest;
+ var urlOfRequest = optionalParams.urlOfRequest;
+
+ if (error || (response.status !== 'success')) {
+ print("Error: unable to complete request from URL. Error:", error || response.status);
+ startNotificationTimer(indexOfRequest);
+ return;
+ }
+
+ if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] ||
+ that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) {
+ startNotificationTimer(indexOfRequest);
+
+ var notificationData;
+ if (concatenatedServerResponse[indexOfRequest].length) {
+ notificationData = concatenatedServerResponse[indexOfRequest];
+ } else {
+ notificationData = that.notificationDataProcessPage[indexOfRequest](response);
+ }
+ console.debug(that.buttonName,
+ 'truncated notification data for processing:',
+ JSON.stringify(notificationData).substring(0, MAX_LOG_LENGTH_CHARACTERS));
+ that.notificationPollCallback[indexOfRequest](notificationData);
+ that.notificationInitialCallbackMade[indexOfRequest] = true;
+ currentDataPageToRetrieve[indexOfRequest] = 1;
+ concatenatedServerResponse[indexOfRequest] = new Array();
+ } else {
+ concatenatedServerResponse[indexOfRequest] =
+ concatenatedServerResponse[indexOfRequest].concat(that.notificationDataProcessPage[indexOfRequest](response));
+ currentDataPageToRetrieve[indexOfRequest]++;
+ request({
+ json: true,
+ uri: (urlOfRequest + "&page=" + currentDataPageToRetrieve[indexOfRequest])
+ }, requestCallback, optionalParams);
+ }
+ }
+
+
+ var METAVERSE_BASE = Account.metaverseServerURL;
+ var MS_IN_SEC = 1000;
+ that.notificationPoll = function (i) {
+ if (!that.notificationPollEndpoint[i]) {
+ return;
+ }
+
+ // User is "appearing offline" or is not logged in
+ if (GlobalServices.findableBy === "none" || Account.username === "Unknown user") {
+ // The notification polling will restart when the user changes their availability
+ // or when they log in, so it's not necessary to restart a timer here.
+ console.debug(that.buttonName + " Notifications: User is appearing offline or not logged in. " +
+ that.buttonName + " will poll for notifications when user logs in and has their availability " +
+ "set to not appear offline.");
+ return;
+ }
+
+ var url = METAVERSE_BASE + that.notificationPollEndpoint[i];
+
+ var settingsKey = "notifications/" + that.notificationPollEndpoint[i] + "/lastPoll";
+ var currentTimestamp = new Date().getTime();
+ var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp);
+ if (that.notificationPollCaresAboutSince[i]) {
+ url = url + "&since=" + lastPollTimestamp / MS_IN_SEC;
+ }
+ Settings.setValue(settingsKey, currentTimestamp);
+
+ request({
+ json: true,
+ uri: url
+ },
+ requestCallback,
+ {
+ indexOfRequest: i,
+ urlOfRequest: url
+ });
+ };
+
+ // This won't do anything if there isn't a notification endpoint set
+ for (i = 0; i < that.notificationPollEndpoint.length; i++) {
+ that.notificationPoll(i);
+ }
+
+ function startNotificationTimer(indexOfRequest) {
+ that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () {
+ that.notificationPoll(indexOfRequest);
+ }, that.notificationPollTimeoutMs[indexOfRequest]);
+ }
+
+ function restartNotificationPoll() {
+ for (var j = 0; j < that.notificationPollEndpoint.length; j++) {
+ that.notificationInitialCallbackMade[j] = false;
+ if (that.notificationPollTimeout[j]) {
+ Script.clearTimeout(that.notificationPollTimeout[j]);
+ that.notificationPollTimeout[j] = false;
+ }
+ that.notificationPoll(j);
+ }
+ }
+ //
+ // END Notification Handling
+ //
+
+ // Properties:
+ that.tablet = Tablet.getTablet(that.tabletName);
+ // Must be after we gather properties.
+ that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-";
+ defaultButton('normalButton', 'i.svg');
+ defaultButton('activeButton', 'a.svg');
+ defaultButton('normalMessagesButton', 'i-msg.svg');
+ defaultButton('activeMessagesButton', 'a-msg.svg');
+ var buttonOptions = {
+ icon: that.normalButton,
+ activeIcon: that.activeButton,
+ text: that.buttonName
+ };
+ // `TabletScriptingInterface` looks for the presence of a `sortOrder` key.
+ // What it SHOULD do is look to see if the value inside that key is defined.
+ // To get around the current code, we do this instead.
+ if (that.sortOrder) {
+ buttonOptions.sortOrder = that.sortOrder;
+ }
+ that.button = that.tablet.addButton(buttonOptions);
+ that.ignore = function ignore() { };
+ that.hasOutboundEventBridge = false;
+ that.hasInboundQmlEventBridge = false;
+ that.hasInboundHtmlEventBridge = false;
+ // HTML event bridge uses strings, not objects. Here we abstract over that.
+ // (Although injected javascript still has to use JSON.stringify/JSON.parse.)
+ that.sendToHtml = function (messageObject) {
+ that.tablet.emitScriptEvent(JSON.stringify(messageObject));
+ };
+ that.fromHtml = function (messageString) {
+ var parsedMessage = JSON.parse(messageString);
+ parsedMessage.messageSrc = "HTML";
+ that.onMessage(parsedMessage);
+ };
+ that.sendMessage = that.ignore;
+ that.wireEventBridge = function wireEventBridge(on) {
+ // Uniquivocally sets that.sendMessage(messageObject) to do the right thing.
+ // Sets has*EventBridge and wires onMessage to the proper event bridge as appropriate, IFF onMessage defined.
+ var isCurrentlyOnQMLScreen = that.isCurrentlyOnQMLScreen();
+ // Outbound (always, regardless of whether there is an inbound handler).
+ if (on) {
+ that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml;
+ that.hasOutboundEventBridge = true;
+ } else {
+ that.sendMessage = that.ignore;
+ that.hasOutboundEventBridge = false;
+ }
+
+ if (!that.onMessage) {
+ return;
+ }
+
+ // Inbound
+ if (on) {
+ if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) {
+ console.debug(that.buttonName, 'connecting', that.tablet.fromQml);
+ that.tablet.fromQml.connect(that.onMessage);
+ that.hasInboundQmlEventBridge = true;
+ } else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) {
+ console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived);
+ that.tablet.webEventReceived.connect(that.fromHtml);
+ that.hasInboundHtmlEventBridge = true;
+ }
+ } else {
+ if (that.hasInboundQmlEventBridge) {
+ console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml);
+ that.tablet.fromQml.disconnect(that.onMessage);
+ that.hasInboundQmlEventBridge = false;
+ }
+ if (that.hasInboundHtmlEventBridge) {
+ console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived);
+ that.tablet.webEventReceived.disconnect(that.fromHtml);
+ that.hasInboundHtmlEventBridge = false;
+ }
+ }
+ };
+ that.isOpen = false;
+ // To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties.
+ that.onClicked = that.home
+ ? function onClicked() {
+ // Call open() or close(), and reset type based on current home property.
+ if (that.isOpen) {
+ that.close();
+ } else {
+ that.open();
+ }
+ } : that.ignore;
+ that.onScriptEnding = function onScriptEnding() {
+ // Close if necessary, clean up any remaining handlers, and remove the button.
+ GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll);
+ GlobalServices.findableByChanged.disconnect(restartNotificationPoll);
+ that.tablet.screenChanged.disconnect(that.onScreenChanged);
+ if (that.isOpen) {
+ that.close();
+ that.onScreenChanged("", "");
+ }
+ if (that.button) {
+ if (that.onClicked) {
+ that.button.clicked.disconnect(that.onClicked);
+ }
+ that.tablet.removeButton(that.button);
+ }
+ for (var i = 0; i < that.notificationPollTimeout.length; i++) {
+ if (that.notificationPollTimeout[i]) {
+ Script.clearInterval(that.notificationPollTimeout[i]);
+ that.notificationPollTimeout[i] = false;
+ }
+ }
+ };
+ // Set up the handlers.
+ that.tablet.screenChanged.connect(that.onScreenChanged);
+ that.button.clicked.connect(that.onClicked);
+ Script.scriptEnding.connect(that.onScriptEnding);
+ GlobalServices.findableByChanged.connect(restartNotificationPoll);
+ GlobalServices.myUsernameChanged.connect(restartNotificationPoll);
+ if (that.buttonName === Settings.getValue("startUpApp")) {
+ Settings.setValue("startUpApp", "");
+ Script.setTimeout(function () {
+ that.open();
+ }, 1000);
+ }
+}
+module.exports = AppUi;
diff --git a/applications/tabletCam/sounds/flashOff.wav b/applications/tabletCam/sounds/flashOff.wav
new file mode 100644
index 0000000..fef7668
Binary files /dev/null and b/applications/tabletCam/sounds/flashOff.wav differ
diff --git a/applications/tabletCam/sounds/flashOn.wav b/applications/tabletCam/sounds/flashOn.wav
new file mode 100644
index 0000000..f7e95c9
Binary files /dev/null and b/applications/tabletCam/sounds/flashOn.wav differ
diff --git a/applications/tabletCam/sounds/snap.wav b/applications/tabletCam/sounds/snap.wav
new file mode 100644
index 0000000..d0143f1
Binary files /dev/null and b/applications/tabletCam/sounds/snap.wav differ
diff --git a/applications/tabletCam/tabletCam_app.js b/applications/tabletCam/tabletCam_app.js
new file mode 100644
index 0000000..a2c1939
--- /dev/null
+++ b/applications/tabletCam/tabletCam_app.js
@@ -0,0 +1,646 @@
+"use strict";
+/*jslint vars:true, plusplus:true, forin:true*/
+/*global Tablet, Script, */
+/* eslint indent: ["error", 4, { "outerIIFEBody": 1 }] */
+//
+// tabletCam_app.js
+//
+// Created by Zach Fox on 2019-04-14
+// Copyright 2022 Overte e.V.
+//
+// Camera with more advanced features than the SNAP application, with an higher resolution capability.
+//
+// Distributed under the Apache License, Version 2.0
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+(function () { // BEGIN LOCAL_SCOPE
+ var AppUi = Script.require('./modules/appUi.js');
+
+ var secondaryCameraConfig = Render.getConfig("SecondaryCamera");
+ var tabletCamAvatarEntity = false;
+ var previousNearClipDistance = false;
+ var previousFarClipDistance = false;
+ var previousvFoV = false;
+ var NEAR_CLIP_DISTANCE = 0.001;
+ var FAR_CLIP_DISTANCE = 16384;
+ var vFoV = Settings.getValue("tabletCam/vFoV", 60);
+ var secondaryCameraResolutionWidth = 1000;
+ var secondaryCameraResolutionHeight = secondaryCameraResolutionWidth / aspectRatio;
+
+ var PREVIEW_SHORT_SIDE_RESOLUTION = 400;
+ var secondaryCameraResolutionPreviewWidth = PREVIEW_SHORT_SIDE_RESOLUTION;
+ var secondaryCameraResolutionPreviewHeight = PREVIEW_SHORT_SIDE_RESOLUTION / aspectRatio;
+
+ var CAMERA_ENTITY_NAME = "CAMERA SNAP-PRO OV-22";
+ var APPLICATION_CAPTION = "SNAP-PRO";
+
+ var tabletCamRunning = false;
+
+ var TABLET_CAM_ENTITY_PROPERTIES = {
+ "type": "Model",
+ "modelURL": Script.resolvePath("models/camera.fbx"),
+ "shapeType": "simple-hull",
+ "dimensions": {"x":0.1600, "y":0.1021, "z":0.1137},
+ "damping": 0,
+ "angularDamping": 0,
+ "shape": "Cube",
+ "isVisibleInSecondaryCamera": false,
+ "name": CAMERA_ENTITY_NAME,
+ "grab": {
+ "grabbable": true
+ },
+ "registrationPoint": {
+ "x": 0.42,
+ "y": 0.4,
+ "z": 0
+ }
+ };
+
+ function enableTabletCam() {
+ if (!tabletCamRunning) {
+ wireSignals(true);
+
+ setTakePhotoControllerMappingStatus(true);
+
+ secondaryCameraConfig.enableSecondaryCameraRenderConfigs(true);
+ setSnapshotQuality(snapshotQuality);
+ var props = TABLET_CAM_ENTITY_PROPERTIES;
+ var dynamicProps = getDynamicTabletCamAvatarEntityProperties();
+ for (var key in dynamicProps) {
+ props[key] = dynamicProps[key];
+ }
+ tabletCamAvatarEntity = Entities.addEntity(props, "avatar");
+ previousFarClipDistance = secondaryCameraConfig.farClipPlaneDistance;
+ previousNearClipDistance = secondaryCameraConfig.nearClipPlaneDistance;
+ previousvFoV = secondaryCameraConfig.vFoV;
+ secondaryCameraConfig.nearClipPlaneDistance = NEAR_CLIP_DISTANCE;
+ secondaryCameraConfig.farClipPlaneDistance = FAR_CLIP_DISTANCE;
+ secondaryCameraConfig.vFoV = vFoV;
+
+ secondaryCameraConfig.attachedEntityId = tabletCamAvatarEntity;
+ tabletCamRunning = true;
+ }
+
+ updateTabletCamLocalEntity();
+
+ // Remove the existing tabletCamAvatarEntity model from the domain if one exists.
+ // It's easy for this to happen if the user crashes while the Tablet Cam is on.
+ // We do this down here (after the new one is rezzed) so that we don't accidentally delete
+ // the newly-rezzed model.
+ var entityIDs = Entities.findEntitiesByName(CAMERA_ENTITY_NAME, MyAvatar.position, 100, false);
+ entityIDs.forEach(function (currentEntityID) {
+ var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID;
+ if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== tabletCamAvatarEntity) {
+ Entities.deleteEntity(currentEntityID);
+ }
+ });
+ }
+
+ var frontCamInUse = Settings.getValue("tabletCam/frontCamInUse", true);
+ function switchCams(forceFrontCamValue) {
+ if (!tabletCamAvatarEntity || (!!HMD.tabletID && !tabletCamLocalEntity)) {
+ console.log("User tried to switch cams, but TabletCam wasn't ready!");
+ return;
+ }
+
+ frontCamInUse = forceFrontCamValue || !frontCamInUse;
+ Settings.setValue("tabletCam/frontCamInUse", frontCamInUse);
+
+ var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties();
+ Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps);
+
+ updateTabletCamLocalEntity();
+ }
+
+ function disableTabletCam() {
+ function deleteTabletCamAvatarEntity() {
+ if (flash) {
+ Entities.deleteEntity(flash);
+ flash = false;
+ }
+
+ if (tabletCamAvatarEntity) {
+ Entities.deleteEntity(tabletCamAvatarEntity);
+ tabletCamAvatarEntity = false;
+ detached = false; // TO BE CONFIRMED
+ Settings.setValue("tabletCam/detached", detached); // TO BE CONFIRMED
+ }
+ }
+
+ wireSignals(false);
+
+ setTakePhotoControllerMappingStatus(false);
+
+ if (tabletCamRunning) {
+ secondaryCameraConfig.farClipPlaneDistance = previousFarClipDistance;
+ secondaryCameraConfig.nearClipPlaneDistance = previousNearClipDistance;
+ secondaryCameraConfig.vFoV = previousvFoV;
+ secondaryCameraConfig.attachedEntityId = false;
+ secondaryCameraConfig.enableSecondaryCameraRenderConfigs(false);
+ }
+
+ deleteTabletCamAvatarEntity();
+
+ if (tabletCamLocalEntity) {
+ Entities.deleteEntity(tabletCamLocalEntity);
+ tabletCamLocalEntity = false;
+ detached = false; // TO BE CONFIRMED
+ Settings.setValue("tabletCam/detached", detached); // TO BE CONFIRMED
+ }
+
+ tabletCamRunning = false;
+ }
+
+ var tabletCamLocalEntityWidth = 0.282;
+ var tabletCamLocalEntityHeight = 0.282;
+ var tabletCamLocalEntityDim = { x: tabletCamLocalEntityWidth, y: tabletCamLocalEntityHeight };
+ var tabletCamLocalEntity = false;
+ var LOCAL_ENTITY_STATIC_PROPERTIES = {
+ type: "Image",
+ imageURL: "resource://spectatorCameraFrame",
+ emissive: true,
+ grab: {
+ "grabbable": false
+ },
+ alpha: 1,
+ triggerable: false
+ };
+ function updateTabletCamLocalEntity() {
+ if (!HMD.tabletID) {
+ return;
+ }
+
+ if (tabletCamLocalEntity) {
+ Entities.deleteEntity(tabletCamLocalEntity);
+ tabletCamLocalEntity = false;
+ }
+ var props = LOCAL_ENTITY_STATIC_PROPERTIES;
+ props.dimensions = tabletCamLocalEntityDim;
+ if (!!HMD.tabletID) {
+ props.parentID = HMD.tabletID;
+ props.localPosition = [0, 0.0225, -0.008];
+ if (frontCamInUse) {
+ props.localRotation = Quat.fromVec3Degrees([0, 180, 180]);
+ } else {
+ props.localRotation = Quat.fromVec3Degrees([0, 0, 180]);
+ }
+ } else {
+ props.parentID = Uuid.NULL;
+ props.localPosition = inFrontOf(0.5);
+ props.localRotation = MyAvatar.orientation;
+ }
+
+ tabletCamLocalEntity = Entities.addEntity(props, "local");
+ }
+
+ function onDomainChanged() {
+ if (tabletCamRunning) {
+ disableTabletCam();
+ }
+ }
+
+ function tabletVisibilityChanged() {
+ if (!ui.tablet.tabletShown && ui.isOpen) {
+ ui.close();
+ }
+ }
+
+ var flash = Settings.getValue("tabletCam/flashEnabled", false);;
+ function setFlashStatus(enabled) {
+ if (!tabletCamAvatarEntity) {
+ return;
+ }
+
+ Settings.setValue("tabletCam/flashEnabled", enabled);
+
+ var cameraPosition = Entities.getEntityProperties(tabletCamAvatarEntity, ["positon"]).position;
+ if (enabled) {
+ Audio.playSound(SOUND_FLASH_ON, {
+ position: cameraPosition,
+ localOnly: true,
+ volume: 0.8
+ });
+ flash = Entities.addEntity({
+ "collisionless": true,
+ "collidesWith": "",
+ "collisionMask": 0,
+ "color": {
+ "blue": 173,
+ "green": 252,
+ "red": 255
+ },
+ "cutoff": 90,
+ "dimensions": {
+ "x": 4,
+ "y": 4,
+ "z": 4
+ },
+ "dynamic": false,
+ "falloffRadius": 0.20000000298023224,
+ "intensity": 27,
+ "isSpotlight": true,
+ "localRotation": { w: 1, x: 0, y: 0, z: 0 },
+ "localPosition": { x: 0, y: 0, z: -0.005 },
+ "name": "Tablet Camera Flash",
+ "type": "Light",
+ "parentID": tabletCamAvatarEntity,
+ }, "avatar");
+ } else {
+ if (flash) {
+ Audio.playSound(SOUND_FLASH_OFF, {
+ position: cameraPosition,
+ localOnly: true,
+ volume: 0.8
+ });
+ Entities.deleteEntity(flash);
+ flash = false;
+ }
+ }
+ }
+
+ function takePhoto() {
+ var tabletCamAvatarEntityPosition = Entities.getEntityProperties(tabletCamAvatarEntity, ["position"]).position;
+ Audio.playSound(SOUND_SNAPSHOT, {
+ position: { x: tabletCamAvatarEntityPosition.x, y: tabletCamAvatarEntityPosition.y, z: tabletCamAvatarEntityPosition.z },
+ localOnly: true,
+ volume: 0.2
+ });
+ Window.takeSecondaryCameraSnapshot();
+ }
+
+ function maybeTakePhoto() {
+ if (tabletCamAvatarEntity) {
+ secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionWidth, secondaryCameraResolutionHeight);
+ // Wait a moment before taking the photo for the resolution to update
+ Script.setTimeout(function () {
+ takePhoto();
+ }, 250);
+ }
+ }
+
+ var snapshotQuality = Settings.getValue("tabletCam/quality", "normal");
+ function setSnapshotQuality(quality) {
+ snapshotQuality = quality;
+ Settings.setValue("tabletCam/quality", snapshotQuality);
+
+ var shortSideTargetResolution = 1000;
+ if (snapshotQuality === "low") {
+ shortSideTargetResolution = 500;
+ } else if (snapshotQuality === "normal") {
+ shortSideTargetResolution = 1000;
+ } else if (snapshotQuality === "high") {
+ shortSideTargetResolution = 2160;
+ } else if (snapshotQuality === "extreme") {
+ shortSideTargetResolution = 4320;
+ }
+
+ if (tallOrientation && !HMD.active) {
+ secondaryCameraResolutionWidth = shortSideTargetResolution;
+ secondaryCameraResolutionHeight = secondaryCameraResolutionWidth / aspectRatio;
+
+ secondaryCameraResolutionPreviewWidth = PREVIEW_SHORT_SIDE_RESOLUTION;
+ secondaryCameraResolutionPreviewHeight = secondaryCameraResolutionPreviewWidth / aspectRatio;
+ } else {
+ secondaryCameraResolutionHeight = shortSideTargetResolution;
+ secondaryCameraResolutionWidth = secondaryCameraResolutionHeight / aspectRatio;
+
+ secondaryCameraResolutionPreviewHeight = PREVIEW_SHORT_SIDE_RESOLUTION;
+ secondaryCameraResolutionPreviewWidth = secondaryCameraResolutionPreviewHeight / aspectRatio;
+ }
+
+ secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionPreviewWidth, secondaryCameraResolutionPreviewHeight);
+ }
+
+ var aspectRatio = parseFloat(Settings.getValue("tabletCam/aspectRatio", "0.8"));
+ function setAspectRatio(ratio) {
+ aspectRatio = ratio;
+ Settings.setValue("tabletCam/aspectRatio", aspectRatio);
+
+ setSnapshotQuality(snapshotQuality);
+ }
+
+ var tallOrientation = Settings.getValue("tabletCam/tallOrientation", true);
+ function setOrientation(orientation) {
+ tallOrientation = orientation;
+ Settings.setValue("tabletCam/tallOrientation", tallOrientation);
+
+ setSnapshotQuality(snapshotQuality);
+ }
+
+ function photoDirChanged(snapshotPath) {
+ Window.browseDirChanged.disconnect(photoDirChanged);
+ if (snapshotPath !== "") { // not cancelled
+ Snapshot.setSnapshotsLocation(snapshotPath);
+ ui.sendMessage({
+ method: "photoDirectoryChanged",
+ photoDirectory: snapshotPath
+ });
+ }
+ }
+
+ function fromQml(message) {
+ switch (message.method) {
+ case 'switchCams':
+ switchCams(message.frontCamInUse);
+ break;
+ case 'switchOrientation':
+ setOrientation(!tallOrientation);
+ break;
+ case 'setFlashStatus':
+ setFlashStatus(message.enabled);
+ break;
+ case 'takePhoto':
+ maybeTakePhoto();
+ break;
+ case 'updateCameravFoV':
+ vFoV = message.vFoV;
+ secondaryCameraConfig.vFoV = vFoV;
+ Settings.setValue("tabletCam/vFoV", vFoV);
+ break;
+ case 'setSnapshotQuality':
+ setSnapshotQuality(message.quality);
+ break;
+ case 'setAspectRatio':
+ setAspectRatio(message.aspectRatio);
+ break;
+ case 'activeViewChanged':
+ if (message.activeView === "settingsView" || message.activeView === "reviewView") {
+ //disableTabletCam();
+ } else {
+ enableTabletCam();
+ }
+ break;
+ case 'setPhotoDirectory':
+ Window.browseDirChanged.connect(photoDirChanged);
+ Window.browseDirAsync("Choose Photo Directory", "", "");
+ break;
+ case 'setDetached':
+ detached = message.detached;
+ Settings.setValue("tabletCam/detached", detached);
+ var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties();
+ Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps);
+ break;
+ default:
+ print('Unrecognized message from TabletCam.qml.');
+ }
+ }
+
+ function setTakePhotoControllerMappingStatus(status) {
+ if (!takePhotoControllerMapping) {
+ return;
+ }
+ if (status) {
+ takePhotoControllerMapping.enable();
+ } else {
+ takePhotoControllerMapping.disable();
+ }
+ }
+
+ var takePhotoControllerMapping;
+ var takePhotoControllerMappingName = 'Hifi-TabletCam-Mapping-TakePhoto';
+ function registerTakePhotoControllerMapping() {
+ takePhotoControllerMapping = Controller.newMapping(takePhotoControllerMappingName);
+ if (controllerType === "OculusTouch") {
+ takePhotoControllerMapping.from(Controller.Standard.RS).to(function (value) {
+ if (value === 1.0) {
+ maybeTakePhoto();
+ }
+ return;
+ });
+ } else if (controllerType === "Vive") {
+ takePhotoControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) {
+ if (value === 1.0) {
+ maybeTakePhoto();
+ }
+ return;
+ });
+ }
+ }
+
+ var controllerType = "Other";
+ function registerButtonMappings() {
+ var VRDevices = Controller.getDeviceNames().toString();
+ if (VRDevices) {
+ if (VRDevices.indexOf("Vive") !== -1) {
+ controllerType = "Vive";
+ } else if (VRDevices.indexOf("OculusTouch") !== -1) {
+ controllerType = "OculusTouch";
+ } else {
+ return; // Neither Vive nor Touch detected
+ }
+ }
+
+ if (!takePhotoControllerMapping) {
+ registerTakePhotoControllerMapping();
+ }
+ }
+
+ function onHMDChanged(isHMDMode) {
+ registerButtonMappings();
+ disableTabletCam();
+ }
+
+ var cameraRollPaths = JSON.parse(Settings.getValue("tabletCam/cameraRollPaths", '{"paths": []}'));
+ function onStillSnapshotTaken(path) {
+ var tempObject = {};
+ tempObject.imagePath = "file:///" + path;
+
+ cameraRollPaths.paths.unshift(tempObject);
+ if (cameraRollPaths.paths.length > 15) {
+ cameraRollPaths.paths.pop();
+ }
+ Settings.setValue("tabletCam/cameraRollPaths", JSON.stringify(cameraRollPaths));
+
+ secondaryCameraConfig.resetSizeSpectatorCamera(secondaryCameraResolutionPreviewWidth, secondaryCameraResolutionPreviewHeight);
+ ui.sendMessage({
+ method: 'stillSnapshotTaken',
+ lastStillSnapshotPath: tempObject.imagePath
+ });
+ }
+
+ var signalsWired = false;
+ function wireSignals(shouldWire) {
+ if (signalsWired === shouldWire) {
+ return;
+ }
+
+ signalsWired = shouldWire;
+
+ if (shouldWire) {
+ Window.stillSnapshotTaken.connect(onStillSnapshotTaken);
+ } else {
+ Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken);
+ }
+ }
+
+ function inFrontOf(distance, position, orientation) {
+ return Vec3.sum(position || MyAvatar.position,
+ Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation)));
+ }
+
+ var detached = false;
+ Settings.setValue("tabletCam/detached", detached);
+ function getDynamicTabletCamAvatarEntityProperties() {
+ var dynamicProps = {
+ dimensions: {"x":0.1600, "y":0.1021, "z":0.1137}
+ };
+
+ if (detached) {
+ //print("DETACHED MODE");
+ dynamicProps.collisionless = false;
+ dynamicProps.ignoreForCollisions = false;
+ dynamicProps.grab = {
+ "grabbable": true,
+ "equippableLeftRotation": {
+ "x": -0.0000152587890625,
+ "y": -0.0000152587890625,
+ "z": -0.0000152587890625,
+ "w": 1
+ },
+ "equippableRightRotation": {
+ "x": -0.0000152587890625,
+ "y": -0.0000152587890625,
+ "z": -0.0000152587890625,
+ "w": 1
+ }
+ };
+ dynamicProps.visible = true;
+ dynamicProps.parentID = Uuid.NULL;
+ dynamicProps.parentJointIndex = 65535;
+ dynamicProps.triggerable = true;
+ if (tabletCamAvatarEntity) {
+ var currentProps = Entities.getEntityProperties(tabletCamAvatarEntity, ["position", "rotation"]);
+ if (!!HMD.tabletID) {
+ dynamicProps.position = inFrontOf(0.2, currentProps.position, currentProps.rotation);
+ } else {
+ dynamicProps.position = currentProps.position;
+ }
+ dynamicProps.rotation = currentProps.rotation;
+ } else {
+ dynamicProps.position = inFrontOf(0.5);
+ dynamicProps.rotation = MyAvatar.orientation;
+ }
+ dynamicProps.velocity = [0, 0, 0];
+ dynamicProps.angularVelocity = [0, 0, 0];
+ } else {
+ dynamicProps.triggerable = false;
+ dynamicProps.collisionless = true;
+ dynamicProps.ignoreForCollisions = true;
+ dynamicProps.grab = {
+ "grabbable": false
+ };
+ dynamicProps.visible = false;
+
+ if (!!HMD.tabletID) {
+ //print("TABLET MODE");
+ dynamicProps.parentID = HMD.tabletID;
+ dynamicProps.parentJointIndex = 65535;
+ dynamicProps.dimensions = [0.01, 0.01, 0.01];
+ } else {
+ //print("DESKTOP USER CAMERA MODE");
+ var cameraMode = Camera.mode;
+ // If:
+ // - User is in third person mode
+ // - User is using the rear-facing camera
+ if (cameraMode !== "first person" && !frontCamInUse) {
+ dynamicProps.parentID = MyAvatar.sessionUUID;
+ dynamicProps.parentJointIndex = MyAvatar.getJointIndex("_CAMERA_MATRIX");
+ } else {
+ dynamicProps.parentID = MyAvatar.sessionUUID;
+ var jointIndex = MyAvatar.getJointIndex("HeadTop_End");
+ if (jointIndex === -1) {
+ jointIndex = MyAvatar.getJointIndex("Head");
+ }
+ dynamicProps.parentJointIndex = jointIndex;
+ }
+ }
+
+ dynamicProps.localPosition = {
+ "x": 0,
+ "y": !!HMD.tabletID ? 0.215 : (frontCamInUse ? -0.03 : (Camera.mode !== "first person" ? 0 : -0.02)),
+ "z": !!HMD.tabletID ? (frontCamInUse ? -0.02 : 0.1) : (frontCamInUse ? 1 : (Camera.mode !== "first person" ? 0 : 0.05))
+ };
+
+ if (!!HMD.tabletID) {
+ dynamicProps.localRotation = {
+ "x": 0,
+ "y": frontCamInUse ? 0 : 1,
+ "z": 0,
+ "w": frontCamInUse ? 1 : 0
+ };
+ } else {
+ dynamicProps.localRotation = {
+ "x": 0,
+ "y": frontCamInUse || (!frontCamInUse && Camera.mode !== "first person") ? 0 : 1,
+ "z": 0,
+ "w": frontCamInUse || (!frontCamInUse && Camera.mode !== "first person") ? 1 : 0
+ };
+ }
+ }
+
+ return dynamicProps;
+ }
+
+ function onModeUpdated(newMode) {
+ if (tabletCamAvatarEntity) {
+ var newTabletCamAvatarEntityProps = getDynamicTabletCamAvatarEntityProperties();
+ Entities.editEntity(tabletCamAvatarEntity, newTabletCamAvatarEntityProps);
+ }
+ }
+
+ function onClosed() {
+ if (!detached) {
+ disableTabletCam();
+ }
+
+ if (tabletCamLocalEntity) {
+ Entities.deleteEntity(tabletCamLocalEntity);
+ tabletCamLocalEntity = false;
+ }
+ }
+
+ function buttonActive(isActive) {
+ ui.button.editProperties({isActive: isActive || tabletCamRunning});
+ }
+
+ var ui;
+ function startup() {
+ ui = new AppUi({
+ buttonName: APPLICATION_CAPTION,
+ home: Script.resolvePath("./ui/TabletCam.qml"),
+ // Selfie by Path Lord from the Noun Project
+ graphicsDirectory: Script.resolvePath("appIcons/"),
+ onOpened: enableTabletCam,
+ onClosed: onClosed,
+ onMessage: fromQml,
+ buttonActive: buttonActive
+ });
+
+ Window.domainChanged.connect(onDomainChanged);
+ ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged);
+ HMD.displayModeChanged.connect(onHMDChanged);
+ Camera.modeUpdated.connect(onModeUpdated);
+
+ registerButtonMappings();
+ }
+ startup();
+
+ function shutdown() {
+ disableTabletCam();
+ Window.domainChanged.disconnect(onDomainChanged);
+ ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
+ HMD.displayModeChanged.disconnect(onHMDChanged);
+ Camera.modeUpdated.disconnect(onModeUpdated);
+ if (takePhotoControllerMapping) {
+ takePhotoControllerMapping.disable();
+ }
+ wireSignals(false);
+ }
+ Script.scriptEnding.connect(shutdown);
+
+ // "Camera Shutter, Fast, A.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
+ var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("sounds/snap.wav"));
+ var SOUND_FLASH_ON = SoundCache.getSound(Script.resolvePath("sounds/flashOn.wav"));
+ var SOUND_FLASH_OFF = SoundCache.getSound(Script.resolvePath("sounds/flashOff.wav"));
+}()); // END LOCAL_SCOPE
diff --git a/applications/tabletCam/ui/TabletCam.qml b/applications/tabletCam/ui/TabletCam.qml
new file mode 100644
index 0000000..40103a7
--- /dev/null
+++ b/applications/tabletCam/ui/TabletCam.qml
@@ -0,0 +1,907 @@
+//
+// TabletCam.qml
+// qml/hifi
+//
+// Tablet Cam v2.2
+//
+// Created by Zach Fox on 2019-04-14
+// Copyright 2022 Overte e.V.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+import Hifi 1.0 as Hifi
+import QtQuick 2.7
+import QtQuick.Controls 2.3
+import stylesUit 1.0 as HifiStylesUit
+import controlsUit 1.0 as HifiControlsUit
+
+Rectangle {
+ id: root;
+ property bool flashEnabled: Settings.getValue("tabletCam/flashEnabled", false);
+ property string snapshotQuality: Settings.getValue("tabletCam/quality", "normal");
+ property real aspectRatio: Settings.getValue("tabletCam/aspectRatio", (8 / 10));
+ property bool detached: Settings.getValue("tabletCam/detached", false);
+ property bool frontCamInUse: Settings.getValue("tabletCam/frontCamInUse", true);
+ property string activeView: "mainView";
+
+ HifiStylesUit.HifiConstants { id: hifi; }
+ color: hifi.colors.black;
+
+ onFlashEnabledChanged: {
+ sendToScript({method: 'setFlashStatus', enabled: root.flashEnabled});
+ }
+
+ onDetachedChanged: {
+ sendToScript({method: 'setDetached', detached: root.detached});
+ }
+
+ onFrontCamInUseChanged: {
+ sendToScript({method: 'switchCams', frontCamInUse: root.frontCamInUse});
+ }
+
+ onActiveViewChanged: {
+ root.flashEnabled = false;
+ sendToScript({method: 'activeViewChanged', activeView: root.activeView});
+
+ if (root.activeView === "settingsView") {
+ photoDirectoryTextField.text = Settings.getValue("snapshotsLocation", "");
+ }
+ }
+
+ Item {
+ id: mainView;
+ visible: root.activeView === "mainView";
+ anchors.fill: parent;
+
+ Rectangle {
+ id: helpTextContainer;
+ visible: !!Settings.getValue('tabletCam/firstRun', true) && HMD.active;
+ width: parent.width;
+ height: topBarContainer_main.height;
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ color: "#121212";
+
+ HifiStylesUit.RalewaySemiBold {
+ text: "Try clicking right thumbstick for photos!";
+ // Anchors
+ anchors.left: parent.left;
+ anchors.leftMargin: 8;
+ anchors.verticalCenter: parent.verticalCenter;
+ size: 22;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignLeft;
+ verticalAlignment: Text.AlignVCenter;
+ wrapMode: Text.Wrap;
+ }
+
+ HifiControlsUit.Button {
+ text: "OK";
+ colorScheme: hifi.colorSchemes.dark;
+ color: hifi.buttons.blue;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.right: parent.right;
+ anchors.rightMargin: 8;
+ width: 50;
+ height: 35;
+ onClicked: {
+ helpTextContainer.visible = false;
+ Settings.setValue('tabletCam/firstRun', false);
+ }
+ }
+ }
+
+ Rectangle {
+ id: topBarContainer_main;
+ visible: !helpTextContainer.visible;
+ width: parent.width;
+ height: 42;
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ color: "#121212";
+
+ HifiControlsUit.CheckBox {
+ id: detachCheckbox;
+ text: "Detach"
+ checked: root.detached;
+ boxSize: 24;
+ height: 32;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.left: parent.left;
+ anchors.leftMargin: 8;
+ onClicked: {
+ root.detached = checked;
+ }
+ }
+
+ HifiControlsUit.GlyphButton {
+ id: flashButton;
+ height: 26;
+ width: height;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.right: fakeFlash.left;
+ anchors.rightMargin: 8;
+ glyph: hifi.glyphs.lightning;
+ color: root.flashEnabled ? hifi.buttons.blue : hifi.buttons.none;
+ onClicked: {
+ root.flashEnabled = !root.flashEnabled;
+ }
+ }
+
+ Rectangle {
+ id: fakeCamera;
+ width: 34;
+ height: width;
+ radius: width;
+ anchors.centerIn: parent;
+ color: hifi.colors.black;
+
+ Rectangle {
+ visible: root.frontCamInUse && !root.detached;
+ width: parent.width - 12;
+ height: width;
+ radius: width;
+ anchors.centerIn: parent;
+ color: "#230000";
+ }
+ }
+
+ Rectangle {
+ id: fakeFlash;
+ width: 12;
+ height: width;
+ radius: width;
+ anchors.verticalCenter: fakeCamera.verticalCenter;
+ anchors.right: fakeCamera.left;
+ anchors.rightMargin: 4;
+ color: root.flashEnabled && root.frontCamInUse ? "#fffcad" : "#000000";
+ }
+
+ Image {
+ id: switchCams;
+ height: 26;
+ width: height;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.left: fakeCamera.right;
+ anchors.leftMargin: 8;
+ source: "./images/switchCams.svg"; // rotate camera by Diego Naive from the Noun Project
+ mipmap: true;
+ MouseArea {
+ anchors.fill: parent;
+ enabled: !root.detached;
+
+ onClicked: {
+ root.frontCamInUse = !root.frontCamInUse;
+ }
+ }
+ }
+
+ HifiControlsUit.GlyphButton {
+ id: settingsButton;
+ height: 26;
+ width: height;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.right: parent.right;
+ anchors.rightMargin: 8;
+ glyph: hifi.glyphs.settings;
+ color: hifi.buttons.none;
+ onClicked: {
+ root.activeView = "settingsView";
+ }
+ }
+ }
+
+ Rectangle {
+ visible: !secondaryCameraPreview.visible && HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}";
+ anchors.fill: secondaryCameraPreview;
+ color: hifi.colors.white;
+ }
+
+ // Secondary Camera Preview
+ Hifi.ResourceImageItem {
+ id: secondaryCameraPreview;
+ visible: HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}";
+ url: "resource://spectatorCameraFrame";
+ ready: visible;
+ mirrorVertically: true;
+ anchors.top: topBarContainer_main.bottom;
+ anchors.bottom: bottomBarContainer_main.top;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ onVisibleChanged: {
+ update();
+ }
+ }
+
+ Rectangle {
+ id: bottomBarContainer_main;
+ height: 88;
+ anchors.left: parent.left;
+ anchors.bottom: parent.bottom;
+ anchors.right: parent.right;
+ color: "#121212";
+
+ Item {
+ id: fieldOfView;
+ anchors.left: parent.left;
+ anchors.leftMargin: 12;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.right: takeSnapshotButton.left;
+ anchors.rightMargin: 12;
+ height: 35;
+
+ HifiControlsUit.GlyphButton {
+ id: resetvFoV;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.left: parent.left;
+ height: parent.height - 8;
+ width: height;
+ glyph: hifi.glyphs.reload;
+ onClicked: {
+ fieldOfViewSlider.value = 60.0;
+ }
+ }
+
+ HifiControlsUit.Slider {
+ id: fieldOfViewSlider;
+ anchors.top: parent.top;
+ anchors.bottom: parent.bottom;
+ anchors.right: parent.right;
+ anchors.left: resetvFoV.right;
+ anchors.leftMargin: 8;
+ colorScheme: hifi.colorSchemes.dark;
+ from: 8.0;
+ to: 120.0;
+ value: (to - Settings.getValue("tabletCam/vFoV", 60.0) + from);
+ stepSize: 1;
+
+ onValueChanged: {
+ sendToScript({method: 'updateCameravFoV', vFoV: to - value + from});
+ }
+ onPressedChanged: {
+ if (!pressed) {
+ sendToScript({method: 'updateCameravFoV', vFoV: to - value + from});
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: takeSnapshotButton;
+ color: "#EA4C5F";
+ anchors.horizontalCenter: parent.horizontalCenter;
+ anchors.verticalCenter: parent.verticalCenter;
+ height: 72;
+ width: height;
+ radius: height;
+ border.width: 3;
+ border.color: hifi.colors.white;
+
+ MouseArea {
+ anchors.fill: parent;
+ hoverEnabled: true;
+ onEntered: {
+ parent.color = "#C62147";
+ }
+ onExited: {
+ parent.color = "#EA4C5F";
+ }
+ onClicked: {
+ if (HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}") {
+ secondaryCameraPreview.visible = false;
+ }
+ sendToScript({method: 'takePhoto'});
+ }
+ }
+ }
+
+ Image {
+ visible: !HMD.active;
+ source: "./images/orientation.svg"; // orientation by Atif Arshad from the Noun Project
+ height: 24;
+ width: height;
+ anchors.left: takeSnapshotButton.right;
+ anchors.leftMargin: 24;
+ anchors.verticalCenter: parent.verticalCenter;
+
+ MouseArea {
+ anchors.fill: parent;
+ onClicked: {
+ sendToScript({method: 'switchOrientation'});
+ }
+ }
+ }
+
+ Rectangle {
+ id: galleryButton;
+ anchors.right: parent.right;
+ anchors.rightMargin: 12;
+ anchors.verticalCenter: parent.verticalCenter;
+ height: 72;
+ width: height;
+ color: hifi.colors.black;
+
+ Image {
+ id: galleryButtonImage;
+ source: JSON.parse(Settings.getValue("tabletCam/cameraRollPaths", '{"paths": ["imagePath": ""]}')).paths[0].imagePath;
+ fillMode: Image.PreserveAspectCrop;
+ anchors.fill: parent;
+ mipmap: true;
+ }
+
+ MouseArea {
+ enabled: galleryButtonImage.source !== "";
+ anchors.fill: parent;
+ onClicked: {
+ cameraRollSwipeView.setCurrentIndex(0);
+ cameraRollModel.clear();
+
+ var settingsString = Settings.getValue("tabletCam/cameraRollPaths", '{"paths": []}');
+ cameraRollModel.append(JSON.parse(settingsString).paths);
+
+ root.activeView = "reviewView";
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: reviewView;
+ visible: root.activeView === "reviewView";
+ anchors.fill: parent;
+
+ Rectangle {
+ id: topBarContainer_review;
+ width: parent.width;
+ height: 42;
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ color: "#121212";
+
+ HifiControlsUit.Button {
+ text: "BACK";
+ colorScheme: hifi.colorSchemes.dark;
+ color: hifi.buttons.noneBorderlessWhite;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.left: parent.left;
+ anchors.leftMargin: 8;
+ width: 50;
+ height: 30;
+ onClicked: {
+ root.activeView = "mainView";
+ }
+ }
+
+ HifiStylesUit.RalewaySemiBold {
+ text: "CAMERA ROLL";
+ // Anchors
+ anchors.horizontalCenter: parent.horizontalCenter;
+ anchors.verticalCenter: parent.verticalCenter;
+ size: 22;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignHCenter;
+ verticalAlignment: Text.AlignVCenter;
+ wrapMode: Text.Wrap;
+ }
+ }
+
+ ListModel {
+ id: cameraRollModel;
+ }
+
+ SwipeView {
+ id: cameraRollSwipeView;
+
+ anchors.top: topBarContainer_review.bottom;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ anchors.bottom: bottomBarContainer_review.top;
+
+ Repeater {
+ model: cameraRollModel;
+
+ Image {
+ source: imagePath;
+ fillMode: Image.PreserveAspectFit;
+ mipmap: true;
+ }
+ }
+ }
+
+ PageIndicator {
+ id: indicator;
+ interactive: true;
+ count: cameraRollSwipeView.count;
+ currentIndex: cameraRollSwipeView.currentIndex
+
+ anchors.bottom: cameraRollSwipeView.bottom;
+ anchors.horizontalCenter: cameraRollSwipeView.horizontalCenter;
+
+ delegate: Rectangle {
+ implicitWidth: 15;
+ implicitHeight: 15;
+ radius: width;
+ color: "#00b4ef";
+ opacity: index === cameraRollSwipeView.currentIndex ? 0.95 : 0.45;
+
+ border.color: "#FFFFFF";
+ border.width: index === cameraRollSwipeView.currentIndex ? 2 : 0;
+
+ Behavior on opacity {
+ OpacityAnimator {
+ duration: 100;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: bottomBarContainer_review;
+ height: 88;
+ anchors.left: parent.left;
+ anchors.bottom: parent.bottom;
+ anchors.right: parent.right;
+ color: "#121212";
+
+ HifiControlsUit.Button {
+ text: "SHOW IN DESKTOP FILE BROWSER";
+ colorScheme: hifi.colorSchemes.dark;
+ color: hifi.buttons.blue;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.horizontalCenter: parent.horizontalCenter;
+ width: 240;
+ height: 30;
+ onClicked: {
+ var currentImagePath = cameraRollModel.get(cameraRollSwipeView.index).imagePath;
+ Qt.openUrlExternally(currentImagePath.substring(0, currentImagePath.lastIndexOf('/')));
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: settingsView;
+ visible: root.activeView === "settingsView";
+ anchors.fill: parent;
+ color: hifi.colors.black;
+
+ Rectangle {
+ id: topBarContainer_settings;
+ width: parent.width;
+ height: 42;
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ color: "#121212";
+
+ HifiControlsUit.Button {
+ text: "BACK";
+ colorScheme: hifi.colorSchemes.dark;
+ color: hifi.buttons.noneBorderlessWhite;
+ anchors.verticalCenter: parent.verticalCenter;
+ anchors.left: parent.left;
+ anchors.leftMargin: 8;
+ width: 50;
+ height: 30;
+ onClicked: {
+ root.activeView = "mainView";
+ }
+ }
+
+ HifiStylesUit.RalewaySemiBold {
+ text: "SETTINGS";
+ // Anchors
+ anchors.horizontalCenter: parent.horizontalCenter;
+ anchors.verticalCenter: parent.verticalCenter;
+ size: 22;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignHCenter;
+ verticalAlignment: Text.AlignVCenter;
+ wrapMode: Text.Wrap;
+ }
+ }
+
+ Item {
+ id: settingsContainer;
+ anchors.top: topBarContainer_settings.bottom;
+ anchors.topMargin: 16;
+ anchors.left: parent.left;
+ anchors.leftMargin: 16;
+ anchors.right: parent.right;
+ anchors.rightMargin: 16;
+ anchors.bottom: parent.bottom;
+ anchors.bottomMargin: 8;
+
+ Item {
+ id: qualityContainer;
+ anchors.top: parent.top;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ height: childrenRect.height;
+
+ HifiStylesUit.RalewaySemiBold {
+ id: qualityHeaderText;
+ text: "Photo Quality";
+ // Anchors
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ height: 22;
+ size: 18;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignLeft;
+ verticalAlignment: Text.AlignTop;
+ }
+
+ HifiControlsUit.RadioButton {
+ id: lowRadioButton;
+ checked: root.snapshotQuality === "low";
+ text: "Low";
+ width: 70;
+ height: 35;
+ anchors.left: parent.left;
+ anchors.top: qualityHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (!lowRadioButton.checked) {
+ lowRadioButton.checked = true;
+ }
+ if (normalRadioButton.checked) {
+ normalRadioButton.checked = false;
+ }
+ if (highRadioButton.checked) {
+ highRadioButton.checked = false;
+ }
+ if (extremeRadioButton.checked) {
+ extremeRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setSnapshotQuality', quality: "low"});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: normalRadioButton;
+ checked: root.snapshotQuality === "normal";
+ text: "Normal";
+ width: 100;
+ height: 35;
+ anchors.left: lowRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: qualityHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (lowRadioButton.checked) {
+ lowRadioButton.checked = false;
+ }
+ if (!normalRadioButton.checked) {
+ normalRadioButton.checked = true;
+ }
+ if (highRadioButton.checked) {
+ highRadioButton.checked = false;
+ }
+ if (extremeRadioButton.checked) {
+ extremeRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setSnapshotQuality', quality: "normal"});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: highRadioButton;
+ checked: root.snapshotQuality === "high";
+ text: "4k";
+ width: 75;
+ height: 35;
+ anchors.left: normalRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: qualityHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (lowRadioButton.checked) {
+ lowRadioButton.checked = false;
+ }
+ if (normalRadioButton.checked) {
+ normalRadioButton.checked = false;
+ }
+ if (!highRadioButton.checked) {
+ highRadioButton.checked = true;
+ }
+ if (extremeRadioButton.checked) {
+ extremeRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setSnapshotQuality', quality: "high"});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: extremeRadioButton;
+ checked: root.snapshotQuality === "extreme";
+ text: "EXTREME";
+ width: 120;
+ height: 35;
+ anchors.left: highRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: qualityHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (lowRadioButton.checked) {
+ lowRadioButton.checked = false;
+ }
+ if (normalRadioButton.checked) {
+ normalRadioButton.checked = false;
+ }
+ if (highRadioButton.checked) {
+ highRadioButton.checked = false;
+ }
+ if (!extremeRadioButton.checked) {
+ extremeRadioButton.checked = true;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setSnapshotQuality', quality: "extreme"});
+ }
+ }
+ }
+ }
+
+ Item {
+ id: aspectRatioContainer;
+ anchors.top: qualityContainer.bottom;
+ anchors.topMargin: 16;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ height: childrenRect.height;
+
+ HifiStylesUit.RalewaySemiBold {
+ id: aspectRatioHeaderText;
+ text: "Aspect Ratio";
+ // Anchors
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ height: 22;
+ size: 18;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignLeft;
+ verticalAlignment: Text.AlignTop;
+ }
+
+ HifiControlsUit.RadioButton {
+ id: eightByTenRadioButton;
+ checked: parseFloat(root.aspectRatio) === (8 / 10);
+ text: "8x10";
+ width: 70;
+ height: 35;
+ anchors.left: parent.left;
+ anchors.top: aspectRatioHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (!eightByTenRadioButton.checked) {
+ eightByTenRadioButton.checked = true;
+ }
+ if (twoByThreeRadioButton.checked) {
+ twoByThreeRadioButton.checked = false;
+ }
+ if (nineBySixteenRadioButton.checked) {
+ nineBySixteenRadioButton.checked = false;
+ }
+ if (oneByOneRadioButton.checked) {
+ oneByOneRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setAspectRatio', aspectRatio: (8 / 10)});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: twoByThreeRadioButton;
+ checked: parseFloat(root.aspectRatio) === (2 / 3);
+ text: "2x3";
+ width: 100;
+ height: 35;
+ anchors.left: eightByTenRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: aspectRatioHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (eightByTenRadioButton.checked) {
+ eightByTenRadioButton.checked = false;
+ }
+ if (!twoByThreeRadioButton.checked) {
+ twoByThreeRadioButton.checked = true;
+ }
+ if (nineBySixteenRadioButton.checked) {
+ nineBySixteenRadioButton.checked = false;
+ }
+ if (oneByOneRadioButton.checked) {
+ oneByOneRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setAspectRatio', aspectRatio: (2 / 3)});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: nineBySixteenRadioButton;
+ checked: parseFloat(root.aspectRatio) === 9 / 16;
+ text: "9x16";
+ width: 75;
+ height: 35;
+ anchors.left: twoByThreeRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: aspectRatioHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (eightByTenRadioButton.checked) {
+ eightByTenRadioButton.checked = false;
+ }
+ if (twoByThreeRadioButton.checked) {
+ twoByThreeRadioButton.checked = false;
+ }
+ if (!nineBySixteenRadioButton.checked) {
+ nineBySixteenRadioButton.checked = true;
+ }
+ if (oneByOneRadioButton.checked) {
+ oneByOneRadioButton.checked = false;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setAspectRatio', aspectRatio: (9 / 16)});
+ }
+ }
+ }
+
+ HifiControlsUit.RadioButton {
+ id: oneByOneRadioButton;
+ checked: parseFloat(root.aspectRatio) === 1 / 1;
+ text: "Square";
+ width: 83;
+ height: 35;
+ anchors.left: nineBySixteenRadioButton.right;
+ anchors.leftMargin: 16;
+ anchors.top: aspectRatioHeaderText.bottom;
+ colorScheme: hifi.colorSchemes.dark;
+ onClicked: {
+ if (eightByTenRadioButton.checked) {
+ eightByTenRadioButton.checked = false;
+ }
+ if (twoByThreeRadioButton.checked) {
+ twoByThreeRadioButton.checked = false;
+ }
+ if (nineBySixteenRadioButton.checked) {
+ nineBySixteenRadioButton.checked = false;
+ }
+ if (!oneByOneRadioButton.checked) {
+ oneByOneRadioButton.checked = true;
+ }
+ }
+ onCheckedChanged: {
+ if (checked) {
+ sendToScript({method: 'setAspectRatio', aspectRatio: 1});
+ }
+ }
+ }
+ }
+
+ Item {
+ id: photoDirectoryContainer;
+ anchors.top: aspectRatioContainer.bottom;
+ anchors.topMargin: 16;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ height: childrenRect.height;
+
+ HifiStylesUit.RalewaySemiBold {
+ id: photoDirectoryHeaderText;
+ text: "Photo Directory";
+ // Anchors
+ anchors.left: parent.left;
+ anchors.top: parent.top;
+ height: 22;
+ size: 18;
+ // Style
+ color: hifi.colors.white;
+ // Alignment
+ horizontalAlignment: Text.AlignLeft;
+ verticalAlignment: Text.AlignTop;
+ }
+
+ HifiControlsUit.TextField {
+ id: photoDirectoryTextField;
+ readOnly: true;
+ text: Settings.getValue("snapshotsDirectory", "");
+ colorScheme: hifi.colorSchemes.dark;
+ // Anchors
+ anchors.top: photoDirectoryHeaderText.bottom;
+ anchors.topMargin: 8;
+ anchors.left: parent.left;
+ anchors.right: parent.right;
+ height: 50;
+
+ MouseArea {
+ anchors.fill: parent;
+
+ onClicked: {
+ sendToScript({method: 'setPhotoDirectory'});
+ }
+ }
+ }
+
+ HifiControlsUit.Button {
+ text: "CHANGE";
+ colorScheme: hifi.colorSchemes.dark;
+ color: hifi.buttons.blue;
+ anchors.top: photoDirectoryTextField.bottom;
+ anchors.topMargin: 4;
+ anchors.right: parent.right;
+ width: 100;
+ height: 35;
+ onClicked: {
+ sendToScript({method: 'setPhotoDirectory'});
+ }
+ }
+ }
+
+ HifiStylesUit.FiraSansRegular {
+ text: "Hint:\nIn HMD, using the detached camera, you can press on the\nthumbsticks of your right controller to take a photo.\n\n\nv2.4";
+ // Anchors
+ anchors.bottom: parent.bottom;
+ anchors.left: parent.left;
+ size: 16;
+ // Style
+ color: hifi.colors.lightGrayText;
+ }
+ }
+ }
+
+ function fromScript(message) {
+ switch (message.method) {
+ case 'stillSnapshotTaken':
+ Settings.setValue('tabletCam/firstRun', false);
+ helpTextContainer.visible = false;
+ galleryButtonImage.source = message.lastStillSnapshotPath;
+ if (HMD.tabletID !== "{00000000-0000-0000-0000-000000000000}") {
+ secondaryCameraPreview.visible = true;
+ }
+ break;
+ case 'photoDirectoryChanged':
+ photoDirectoryTextField.text = message.photoDirectory;
+ break;
+ case 'inspectionCertificate_resetCert':
+ break;
+ default:
+ console.log('Unrecognized message from TabletCam.js.');
+ }
+ }
+ signal sendToScript(var message);
+}
diff --git a/applications/tabletCam/ui/images/orientation.svg b/applications/tabletCam/ui/images/orientation.svg
new file mode 100644
index 0000000..9bf7520
--- /dev/null
+++ b/applications/tabletCam/ui/images/orientation.svg
@@ -0,0 +1,3 @@
+
diff --git a/applications/tabletCam/ui/images/switchCams.svg b/applications/tabletCam/ui/images/switchCams.svg
new file mode 100644
index 0000000..28c752e
--- /dev/null
+++ b/applications/tabletCam/ui/images/switchCams.svg
@@ -0,0 +1,9 @@
+