Merge branch 'exportToTestRail' of github.com:NissimHadar/hifi into exportToTestRail

This commit is contained in:
NissimHadar 2018-07-27 08:09:22 -07:00
commit 78cf29619e
62 changed files with 1840 additions and 411 deletions

View file

@ -22,7 +22,8 @@
android:icon="@drawable/ic_launcher"
android:launchMode="singleTop"
android:roundIcon="@drawable/ic_launcher">
<activity android:name="io.highfidelity.hifiinterface.PermissionChecker">
<activity android:name="io.highfidelity.hifiinterface.PermissionChecker"
android:theme="@style/Theme.AppCompat.Translucent.NoActionBar.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View file

@ -162,7 +162,19 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea
jvmArgs.version = JNI_VERSION_1_6; // choose your JNI version
jvmArgs.name = NULL; // you might want to give the java thread a name
jvmArgs.group = NULL; // you might want to assign the java thread to a ThreadGroup
jvm->AttachCurrentThread(reinterpret_cast<JNIEnv **>(&myNewEnv), &jvmArgs);
int attachedHere = 0; // know if detaching at the end is necessary
jint res = jvm->GetEnv((void**)&myNewEnv, JNI_VERSION_1_6); // checks if current env needs attaching or it is already attached
if (JNI_OK != res) {
qDebug() << "[JCRASH] GetEnv env not attached yet, attaching now..";
res = jvm->AttachCurrentThread(reinterpret_cast<JNIEnv **>(&myNewEnv), &jvmArgs);
if (JNI_OK != res) {
qDebug() << "[JCRASH] Failed to AttachCurrentThread, ErrorCode = " << res;
return;
} else {
attachedHere = 1;
}
}
QAndroidJniObject string = QAndroidJniObject::fromString(a);
jboolean jBackToScene = (jboolean) backToScene;
@ -175,7 +187,9 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea
myNewEnv->CallObjectMethod(hashmap, mapClassPut, QAndroidJniObject::fromString("url").object<jstring>(), jArg.object<jstring>());
}
__interfaceActivity.callMethod<void>("openAndroidActivity", "(Ljava/lang/String;ZLjava/util/HashMap;)V", string.object<jstring>(), jBackToScene, hashmap);
jvm->DetachCurrentThread();
if (attachedHere) {
jvm->DetachCurrentThread();
}
});
QObject::connect(&AndroidHelper::instance(), &AndroidHelper::hapticFeedbackRequested, [](int duration) {

View file

@ -8,6 +8,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import org.json.JSONException;
import org.json.JSONObject;
@ -135,4 +136,13 @@ public class PermissionChecker extends Activity {
launchActivityWithPermissions();
}
}
@Override
protected void onResume() {
super.onResume();
View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
}
}

View file

@ -66,6 +66,7 @@ public class HomeFragment extends Fragment {
GridLayoutManager gridLayoutMgr = new GridLayoutManager(getContext(), numberOfColumns);
mDomainsView.setLayoutManager(gridLayoutMgr);
mDomainAdapter = new DomainAdapter(getContext(), HifiUtils.getInstance().protocolVersionSignature(), nativeGetLastLocation());
mSwipeRefreshLayout.setRefreshing(true);
mDomainAdapter.setClickListener((view, position, domain) -> {
new Handler(getActivity().getMainLooper()).postDelayed(() -> {
if (mListener != null) {

View file

@ -60,6 +60,8 @@ import android.view.View;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import org.qtproject.qt5.android.QtNative;
@SuppressWarnings("unused")
public class QtActivity extends Activity {
public String APPLICATION_PARAMETERS = null;
@ -103,7 +105,7 @@ public class QtActivity extends Activity {
}
public boolean super_dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
return super_dispatchPopulateAccessibilityEvent(event);
return super.dispatchPopulateAccessibilityEvent(event);
}
//---------------------------------------------------------------------------
@ -362,7 +364,25 @@ public class QtActivity extends Activity {
@Override
protected void onDestroy() {
super.onDestroy();
QtApplication.invokeDelegate();
/*
cduarte https://highfidelity.manuscript.com/f/cases/16712/App-freezes-on-opening-randomly
After Qt upgrade to 5.11 we had a black screen crash after closing the application with
the hardware button "Back" and trying to start the app again. It could only be fixed after
totally closing the app swiping it in the list of running apps.
This problem did not happen with the previous Qt version.
After analysing changes we came up with this case and change:
https://codereview.qt-project.org/#/c/218882/
In summary they've moved libs loading to the same thread as main() and as a matter of correctness
in the onDestroy method in QtActivityDelegate, they exit that thread with `QtNative.m_qtThread.exit();`
That exit call is the main reason of this problem.
In this fix we just replace the `QtApplication.invokeDelegate();` call that may end using the
entire onDestroy method including that thread exit line for other three lines that purposely
terminate qt (borrowed from QtActivityDelegate::onDestroy as well).
*/
QtNative.terminateQt();
QtNative.setActivity(null, null);
System.exit(0);
}
//---------------------------------------------------------------------------

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<item android:drawable="@android:color/black"/>
<item android:drawable="@drawable/hifi_logo_splash"
android:gravity="center_horizontal"
android:top="225dp"
android:height="242dp"
android:width="242dp">
</item>
</layer-list>

View file

@ -7,9 +7,17 @@
android:layout_height="match_parent"
android:background="@android:color/black">
<ImageView
android:id="@+id/hifi_logo"
android:layout_width="242dp"
android:layout_height="242dp"
android:src="@drawable/hifi_logo_splash"
android:layout_centerHorizontal="true"
android:layout_marginTop="225dp"/>
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar"
android:theme="@style/HifiCircularProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/hifi_logo"
android:layout_centerHorizontal="true"/>
</RelativeLayout>

View file

@ -17,4 +17,5 @@
<color name="colorLoginError">#FF7171</color>
<color name="black_060">#99000000</color>
<color name="statusbar_color">#292929</color>
<color name="hifiLogoColor">#23B2E7</color>
</resources>

View file

@ -19,6 +19,9 @@
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.AppCompat.Translucent.NoActionBar.Launcher" parent="Theme.AppCompat.Translucent.NoActionBar">
<item name="android:windowBackground">@drawable/launch_screen</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
@ -62,4 +65,7 @@
<item name="android:background">@color/white_opaque</item>
</style>
<style name="HifiCircularProgress" parent="Theme.AppCompat.Light">
<item name="colorAccent">@color/hifiLogoColor</item>
</style>
</resources>

View file

@ -1223,7 +1223,7 @@
"name": "max_avatar_height",
"type": "double",
"label": "Maximum Avatar Height (meters)",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.",
"help": "Limits the height of avatars in your domain. Cannot be greater than 1755.",
"placeholder": 5.2,
"default": 5.2
},

View file

@ -58,7 +58,11 @@ $(document).ready(function(){
}
Settings.handlePostSettings = function(formJSON) {
if (!verifyAvatarHeights()) {
return false;
}
// check if we've set the basic http password
if (formJSON["security"]) {
@ -207,7 +211,7 @@ $(document).ready(function(){
swal({
title: '',
type: 'error',
text: "There was a problem retreiving domain information from High Fidelity API.",
text: "There was a problem retrieving domain information from High Fidelity API.",
confirmButtonText: 'Try again',
showCancelButton: true,
closeOnConfirm: false
@ -288,7 +292,7 @@ $(document).ready(function(){
swal({
title: 'Create new domain ID',
type: 'input',
text: 'Enter a label this machine.</br></br>This will help you identify which domain ID belongs to which machine.</br></br>',
text: 'Enter a label for this machine.</br></br>This will help you identify which domain ID belongs to which machine.</br></br>',
showCancelButton: true,
confirmButtonText: "Create",
closeOnConfirm: false,
@ -669,7 +673,7 @@ $(document).ready(function(){
var spinner = createDomainSpinner();
$('#' + Settings.PLACES_TABLE_ID).after($(spinner));
var errorEl = createDomainLoadingError("There was an error retreiving your places.");
var errorEl = createDomainLoadingError("There was an error retrieving your places.");
$("#" + Settings.PLACES_TABLE_ID).after(errorEl);
// do we have a domain ID?
@ -1091,4 +1095,43 @@ $(document).ready(function(){
$('#settings_backup .panel-body').html(html);
}
function verifyAvatarHeights() {
var errorString = '';
var minAllowedHeight = 0.009;
var maxAllowedHeight = 1755;
var alertCss = { backgroundColor: '#ffa0a0' };
var minHeightElement = $('input[name="avatars.min_avatar_height"]');
var maxHeightElement = $('input[name="avatars.max_avatar_height"]');
var minHeight = Number(minHeightElement.val());
var maxHeight = Number(maxHeightElement.val());
if (maxHeight < minHeight) {
errorString = 'Maximum avatar height must not be less than minimum avatar height<br>';
minHeightElement.css(alertCss);
maxHeightElement.css(alertCss);
};
if (minHeight < minAllowedHeight) {
errorString += 'Minimum avatar height must not be less than ' + minAllowedHeight + '<br>';
minHeightElement.css(alertCss);
}
if (maxHeight > maxAllowedHeight) {
errorString += 'Maximum avatar height must not be greater than ' + maxAllowedHeight + '<br>';
maxHeightElement.css(alertCss);
}
if (errorString.length > 0) {
swal({
type: 'error',
title: '',
text: errorString,
html: true
});
return false;
} else {
return true;
}
}
});

View file

@ -1916,14 +1916,16 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// don't handle if we don't have a matching node
if (!matchingNode) {
return false;
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
return true;
}
auto nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
// don't handle if we don't have node data for this node
if (!nodeData) {
return false;
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
return true;
}
SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID());
@ -1944,7 +1946,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
// request not handled
return false;
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
return true;
}
// check if this is a request for our domain ID

View file

@ -26,6 +26,7 @@ ScrollingWindow {
y: 100
Component.onCompleted: {
focus = true
shown = true
addressBar.text = webview.url
}

View file

@ -53,7 +53,7 @@ Column {
'protocol=' + encodeURIComponent(Window.protocolSignature())
];
endpoint: '/api/v1/user_stories?' + options.join('&');
itemsPerPage: 3;
itemsPerPage: 4;
processPage: function (data) {
return data.user_stories.map(makeModelData);
};
@ -106,7 +106,6 @@ Column {
highlightMoveDuration: -1;
highlightMoveVelocity: -1;
currentIndex: -1;
onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } }
spacing: 12;
width: parent.width;

View file

@ -61,7 +61,7 @@ Rectangle {
'username';
}
sortAscending: connectionsTable.sortIndicatorOrder === Qt.AscendingOrder;
itemsPerPage: 9;
itemsPerPage: 10;
listView: connectionsTable;
processPage: function (data) {
return data.users.map(function (user) {
@ -786,14 +786,6 @@ Rectangle {
}
model: connectionsUserModel;
Connections {
target: connectionsTable.flickableItem;
onAtYEndChanged: {
if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) {
connectionsUserModel.getNextPage();
}
}
}
// This Rectangle refers to each Row in the connectionsTable.
rowDelegate: Rectangle {

View file

@ -125,6 +125,7 @@ Rectangle {
id: wearablesCombobox
anchors.left: parent.left
anchors.right: parent.right
comboBox.textRole: "text"
model: ListModel {
function findIndexById(id) {

View file

@ -398,7 +398,7 @@ Item {
http: root.http;
listModelName: root.listModelName;
endpoint: "/api/v1/users?filter=connections";
itemsPerPage: 8;
itemsPerPage: 9;
listView: connectionsList;
processPage: function (data) {
return data.users;
@ -520,7 +520,6 @@ Item {
visible: !connectionsLoading.visible;
clip: true;
model: connectionsModel;
onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); }
snapMode: ListView.SnapToItem;
// Anchors
anchors.fill: parent;

View file

@ -335,7 +335,8 @@ Item {
upgradeUrl: root.upgradeUrl,
itemHref: root.itemHref,
itemType: root.itemType,
isInstalled: root.isInstalled
isInstalled: root.isInstalled,
wornEntityID: root.wornEntityID
});
}
}

View file

@ -551,8 +551,9 @@ Rectangle {
HifiModels.PSFListModel {
id: purchasesModel;
itemsPerPage: 6;
itemsPerPage: 7;
listModelName: 'purchases';
listView: purchasesContentsList;
getPage: function () {
console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage);
Commerce.inventory(
@ -706,6 +707,12 @@ Rectangle {
}
}
} else if (msg.method === "updateItemClicked") {
// These three cases are very similar to the conditionals below, under
// "if msg.method === "giftAsset". They differ in their popup's wording
// and the actions to take when continuing.
// I could see an argument for DRYing up this code, but I think the
// actions are different enough now and potentially moving forward such that I'm
// OK with "somewhat repeating myself".
if (msg.itemType === "app" && msg.isInstalled) {
lightboxPopup.titleText = "Uninstall App";
lightboxPopup.bodyText = "The app that you are trying to update is installed.<br><br>" +
@ -720,6 +727,35 @@ Rectangle {
sendToScript(msg);
};
lightboxPopup.visible = true;
} else if (msg.itemType === "wearable" && msg.wornEntityID !== '') {
lightboxPopup.titleText = "Remove Wearable";
lightboxPopup.bodyText = "You are currently wearing the wearable that you are trying to update.<br><br>" +
"If you proceed, this wearable will be removed.";
lightboxPopup.button1text = "CANCEL";
lightboxPopup.button1method = function() {
lightboxPopup.visible = false;
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Entities.deleteEntity(msg.wornEntityID);
purchasesModel.setProperty(index, 'wornEntityID', '');
sendToScript(msg);
};
lightboxPopup.visible = true;
} else if (msg.itemType === "avatar" && MyAvatar.skeletonModelURL === msg.itemHref) {
lightboxPopup.titleText = "Change Avatar to Default";
lightboxPopup.bodyText = "You are currently wearing the avatar that you are trying to update.<br><br>" +
"If you proceed, your avatar will be changed to the default avatar.";
lightboxPopup.button1text = "CANCEL";
lightboxPopup.button1method = function() {
lightboxPopup.visible = false;
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
MyAvatar.useFullAvatarURL('');
sendToScript(msg);
};
lightboxPopup.visible = true;
} else {
sendToScript(msg);
}
@ -781,14 +817,6 @@ Rectangle {
}
}
}
onAtYEndChanged: {
if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) {
console.log("User scrolled to the bottom of 'Purchases'.");
purchasesModel.getNextPage();
}
}
}
Rectangle {

View file

@ -212,6 +212,7 @@ Item {
HifiModels.PSFListModel {
id: transactionHistoryModel;
listModelName: "transaction history"; // For debugging. Alternatively, we could specify endpoint for that purpose, even though it's not used directly.
listView: transactionHistory;
itemsPerPage: 6;
getPage: function () {
console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve);
@ -346,12 +347,6 @@ Item {
}
}
}
onAtYEndChanged: {
if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) {
console.log("User scrolled to the bottom of 'Recent Activity'.");
transactionHistoryModel.getNextPage();
}
}
}
Item {

View file

@ -33,7 +33,6 @@ ListModel {
// QML fires the following changed handlers even when first instantiating the Item. So we need a guard against firing them too early.
property bool initialized: false;
Component.onCompleted: initialized = true;
onEndpointChanged: if (initialized) { getFirstPage('delayClear'); }
onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); }
onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); }
@ -60,7 +59,37 @@ ListModel {
// Override to return one property of data, and/or to transform the elements. Must return an array of model elements.
property var processPage: function (data) { return data; }
property var listView; // Optional. For debugging.
property var listView; // Optional. For debugging, or for having the scroll handler automatically call getNextPage.
property var flickable: listView && (listView.flickableItem || listView);
// 2: get two pages before you need it (i.e. one full page before you reach the end).
// 1: equivalent to paging when reaching end (and not before).
// 0: don't getNextPage on scroll at all here. The application code will do it.
property real pageAhead: 2.0;
function needsEarlyYFetch() {
return flickable
&& !flickable.atYBeginning
&& (flickable.contentY - flickable.originY) >= (flickable.contentHeight - (pageAhead * flickable.height));
}
function needsEarlyXFetch() {
return flickable
&& !flickable.atXBeginning
&& (flickable.contentX - flickable.originX) >= (flickable.contentWidth - (pageAhead * flickable.width));
}
function getNextPageIfHorizontalScroll() {
if (needsEarlyXFetch()) { getNextPage(); }
}
function getNextPageIfVerticalScroll() {
if (needsEarlyYFetch()) { getNextPage(); }
}
Component.onCompleted: {
initialized = true;
if (flickable && pageAhead > 0.0) {
// Pun: Scrollers are usually one direction or another, such that only one of the following will actually fire.
flickable.contentXChanged.connect(getNextPageIfHorizontalScroll);
flickable.contentYChanged.connect(getNextPageIfVerticalScroll);
}
}
property int totalPages: 0;
property int totalEntries: 0;
// Check consistency and call processPage.

View file

@ -299,7 +299,7 @@ Item {
anchors.fill: stackView
id: controllerPrefereneces
objectName: "TabletControllerPreferences"
showCategories: ["VR Movement", "Game Controller", "Sixense Controllers", "Perception Neuron", "Leap Motion"]
showCategories: [( (HMD.active) ? "VR Movement" : "Movement"), "Game Controller", "Sixense Controllers", "Perception Neuron", "Leap Motion"]
categoryProperties: {
"VR Movement" : {
"User real-world height (meters)" : { "anchors.right" : "undefined" },

View file

@ -3650,6 +3650,10 @@ bool Application::event(QEvent* event) {
bool Application::eventFilter(QObject* object, QEvent* event) {
if (_aboutToQuit) {
return true;
}
if (event->type() == QEvent::Leave) {
getApplicationCompositor().handleLeaveEvent();
}
@ -4839,6 +4843,13 @@ void Application::loadSettings() {
}
isFirstPerson = (qApp->isHMDMode());
// Flying should be disabled by default in HMD mode on first run, and it
// should be enabled by default in desktop mode.
auto myAvatar = getMyAvatar();
myAvatar->setFlyingEnabled(!isFirstPerson);
} else {
// if this is not the first run, the camera will be initialized differently depending on user settings

View file

@ -144,9 +144,19 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) {
emit bookmarkDeleted(bookmarkName);
}
bool isWearableEntity(const EntityItemPointer& entity) {
return entity->isVisible() && entity->getParentJointIndex() != INVALID_JOINT_INDEX &&
(entity->getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || entity->getParentID() == DependencyManager::get<AvatarManager>()->getMyAvatar()->getSelfID());
}
void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
myAvatar->removeAvatarEntities();
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
myAvatar->removeAvatarEntities([&](const QUuid& entityID) {
auto entity = entityTree->findEntityByID(entityID);
return entity && isWearableEntity(entity);
});
addAvatarEntities(avatarEntities);
}
@ -163,7 +173,12 @@ void AvatarBookmarks::loadBookmark(const QString& bookmarkName) {
QVariantMap bookmark = bookmarkEntry.value().toMap();
if (!bookmark.empty()) {
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
myAvatar->removeAvatarEntities();
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
myAvatar->removeAvatarEntities([&](const QUuid& entityID) {
auto entity = entityTree->findEntityByID(entityID);
return entity && isWearableEntity(entity);
});
const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString();
myAvatar->useFullAvatarURL(avatarUrl);
qCDebug(interfaceapp) << "Avatar On " << avatarUrl;
@ -233,6 +248,27 @@ QVariantMap AvatarBookmarks::getAvatarDataToBookmark() {
bookmark.insert(ENTRY_AVATAR_URL, avatarUrl);
bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale);
bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant());
bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant());
QScriptEngine scriptEngine;
QVariantList wearableEntities;
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
auto avatarEntities = myAvatar->getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
auto entity = entityTree->findEntityByID(entityID);
if (!entity || !isWearableEntity(entity)) {
continue;
}
QVariantMap avatarEntityData;
EncodeBitstreamParams params;
auto desiredProperties = entity->getEntityProperties(params);
desiredProperties += PROP_LOCAL_POSITION;
desiredProperties += PROP_LOCAL_ROTATION;
EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties);
avatarEntityData["properties"] = scriptProperties.toVariant();
wearableEntities.append(QVariant(avatarEntityData));
}
bookmark.insert(ENTRY_AVATAR_ENTITIES, wearableEntities);
return bookmark;
}

View file

@ -275,6 +275,13 @@ Menu::Menu() {
QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog");
});
// Settings > Attachments...
action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Attachments);
connect(action, &QAction::triggered, [] {
qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"),
QString("hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog");
});
// Settings > Developer Menu
addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus()));

View file

@ -1281,7 +1281,8 @@ void MyAvatar::loadData() {
settings.remove("avatarEntityData");
}
setAvatarEntityDataChanged(true);
setFlyingEnabled(settings.value("enabledFlying").toBool());
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
setFlyingEnabled(firstRunVal.get() ? getFlyingEnabled() : settings.value("enabledFlying").toBool());
setDisplayName(settings.value("displayName").toString());
setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString());
setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool());
@ -1607,14 +1608,16 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
emit skeletonModelURLChanged();
}
void MyAvatar::removeAvatarEntities() {
void MyAvatar::removeAvatarEntities(const std::function<bool(const QUuid& entityID)>& condition) {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
entityTree->withWriteLock([&] {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
entityTree->deleteEntity(entityID, true, true);
if (!condition || condition(entityID)) {
entityTree->deleteEntity(entityID, true, true);
}
}
});
}

View file

@ -33,6 +33,7 @@
#include "MyCharacterController.h"
#include "RingBufferHistory.h"
#include <ThreadSafeValueCache.h>
#include <EntityItem.h>
class AvatarActionHold;
class ModelItemID;
@ -926,7 +927,7 @@ public:
* @returns {object[]}
*/
Q_INVOKABLE QVariantList getAvatarEntitiesVariant();
void removeAvatarEntities();
void removeAvatarEntities(const std::function<bool(const QUuid& entityID)>& condition = {});
/**jsdoc
* @function MyAvatar.isFlying

View file

@ -262,6 +262,9 @@ int main(int argc, const char* argv[]) {
// Extend argv to enable WebGL rendering
std::vector<const char*> argvExtended(&argv[0], &argv[argc]);
argvExtended.push_back("--ignore-gpu-blacklist");
#ifdef Q_OS_ANDROID
argvExtended.push_back("--suppress-settings-reset");
#endif
int argcExtended = (int)argvExtended.size();
PROFILE_SYNC_END(startup, "main startup", "");

View file

@ -56,6 +56,7 @@ Audio::Audio() : _devices(_contextIsHMD) {
connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume);
connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged);
enableNoiseReduction(enableNoiseReductionSetting.get());
onContextChanged();
}
bool Audio::startRecording(const QString& filepath) {

View file

@ -172,11 +172,13 @@ void LoginDialog::openUrl(const QString& url) const {
offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) {
newObject->setProperty("url", url);
});
LoginDialog::hide();
} else {
if (!hmd->getShouldShowTablet() && !qApp->isHMDMode()) {
offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) {
newObject->setProperty("url", url);
});
LoginDialog::hide();
} else {
tablet->gotoWebScreen(url);
}

View file

@ -266,20 +266,15 @@ void setupPreferences() {
preferences->addPreference(new SliderPreference(FACE_TRACKING, "Eye Deflection", getter, setter));
}
static const QString MOVEMENT{ "VR Movement" };
static const QString MOVEMENT{ "Movement" };
{
static const QString movementsControlChannel = QStringLiteral("Hifi-Advanced-Movement-Disabler");
auto getter = [=]()->bool { return myAvatar->useAdvancedMovementControls(); };
auto setter = [=](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
preferences->addPreference(new CheckPreference(MOVEMENT,
QStringLiteral("Advanced movement for hand controllers"),
getter, setter));
}
{
auto getter = [=]()->bool { return myAvatar->getFlyingEnabled(); };
auto setter = [=](bool value) { myAvatar->setFlyingEnabled(value); };
preferences->addPreference(new CheckPreference(MOVEMENT, "Flying & jumping", getter, setter));
QStringLiteral("Advanced movement for hand controllers"),
getter, setter));
}
{
auto getter = [=]()->int { return myAvatar->getSnapTurn() ? 0 : 1; };
@ -307,6 +302,47 @@ void setupPreferences() {
preferences->addPreference(preference);
}
static const QString VR_MOVEMENT{ "VR Movement" };
{
static const QString movementsControlChannel = QStringLiteral("Hifi-Advanced-Movement-Disabler");
auto getter = [=]()->bool { return myAvatar->useAdvancedMovementControls(); };
auto setter = [=](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
preferences->addPreference(new CheckPreference(VR_MOVEMENT,
QStringLiteral("Advanced movement for hand controllers"),
getter, setter));
}
{
auto getter = [=]()->bool { return myAvatar->getFlyingEnabled(); };
auto setter = [=](bool value) { myAvatar->setFlyingEnabled(value); };
preferences->addPreference(new CheckPreference(VR_MOVEMENT, "Flying & jumping", getter, setter));
}
{
auto getter = [=]()->int { return myAvatar->getSnapTurn() ? 0 : 1; };
auto setter = [=](int value) { myAvatar->setSnapTurn(value == 0); };
auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Snap turn / Smooth turn", getter, setter);
QStringList items;
items << "Snap turn" << "Smooth turn";
preference->setItems(items);
preferences->addPreference(preference);
}
{
auto getter = [=]()->float { return myAvatar->getUserHeight(); };
auto setter = [=](float value) { myAvatar->setUserHeight(value); };
auto preference = new SpinnerPreference(VR_MOVEMENT, "User real-world height (meters)", getter, setter);
preference->setMin(1.0f);
preference->setMax(2.2f);
preference->setDecimals(3);
preference->setStep(0.001f);
preferences->addPreference(preference);
}
{
auto preference = new ButtonPreference(VR_MOVEMENT, "RESET SENSORS", [] {
qApp->resetSensors();
});
preferences->addPreference(preference);
}
static const QString AVATAR_CAMERA{ "Mouse Sensitivity" };
{
auto getter = [=]()->float { return myAvatar->getPitchSpeed(); };

View file

@ -27,6 +27,12 @@ ModelOverlay::ModelOverlay()
{
_model->setLoadingPriority(_loadPriority);
_isLoaded = false;
// Don't show overlay until textures have loaded
_visible = false;
render::ScenePointer scene = qApp->getMain3DScene();
_model->setVisibleInScene(false, scene);
}
ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) :
@ -101,10 +107,11 @@ void ModelOverlay::update(float deltatime) {
emit DependencyManager::get<scriptable::ModelProviderFactory>()->modelAddedToScene(getID(), NestableType::Overlay, _model);
}
bool metaDirty = false;
if (_visibleDirty) {
if (_visibleDirty && _texturesLoaded) {
_visibleDirty = false;
// don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true
uint8_t modelRenderTagMask = (_isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW);
_model->setTagMask(modelRenderTagMask, scene);
_model->setVisibleInScene(getVisible(), scene);
metaDirty = true;
@ -134,6 +141,8 @@ void ModelOverlay::update(float deltatime) {
if (!_modelTextures.isEmpty()) {
_model->setTextures(_modelTextures);
}
_model->setVisibleInScene(getVisible(), scene);
_model->updateRenderItems();
}
}

View file

@ -138,11 +138,8 @@ void Web3DOverlay::destroyWebSurface() {
// Fix for crash in QtWebEngineCore when rapidly switching domains
// Call stop on the QWebEngineView before destroying OffscreenQMLSurface.
if (rootItem) {
QObject* obj = rootItem->findChild<QObject*>("webEngineView");
if (obj) {
// stop loading
QMetaObject::invokeMethod(obj, "stop");
}
// stop loading
QMetaObject::invokeMethod(rootItem, "stop");
}
_webSurface->pause();
@ -152,6 +149,11 @@ void Web3DOverlay::destroyWebSurface() {
// If the web surface was fetched out of the cache, release it back into the cache
if (_cachedWebSurface) {
// If it's going back into the cache make sure to explicitly set the URL to a blank page
// in order to stop any resource consumption or audio related to the page.
if (rootItem) {
rootItem->setProperty("url", "about:blank");
}
auto offscreenCache = DependencyManager::get<OffscreenQmlSurfaceCache>();
// FIXME prevents crash on shutdown, but we shoudln't have to do this check
if (offscreenCache) {

View file

@ -1,5 +1,8 @@
set(TARGET_NAME audio-client)
setup_hifi_library(Network Multimedia)
if (ANDROID)
set(PLATFORM_QT_COMPONENTS AndroidExtras)
endif ()
setup_hifi_library(Network Multimedia ${PLATFORM_QT_COMPONENTS})
link_hifi_libraries(audio plugins)
include_hifi_library_headers(shared)
include_hifi_library_headers(networking)

View file

@ -52,6 +52,10 @@
#include "AudioLogging.h"
#include "AudioHelpers.h"
#if defined(Q_OS_ANDROID)
#include <QtAndroidExtras/QAndroidJniObject>
#endif
const int AudioClient::MIN_BUFFER_FRAMES = 1;
const int AudioClient::MAX_BUFFER_FRAMES = 20;
@ -60,7 +64,7 @@ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 100;
#if defined(Q_OS_ANDROID)
static const int CHECK_INPUT_READS_MSECS = 2000;
static const int MIN_READS_TO_CONSIDER_INPUT_ALIVE = 100;
static const int MIN_READS_TO_CONSIDER_INPUT_ALIVE = 10;
#endif
static const auto DEFAULT_POSITION_GETTER = []{ return Vectors::ZERO; };
@ -235,7 +239,7 @@ AudioClient::AudioClient() :
// start a thread to detect any device changes
_checkDevicesTimer = new QTimer(this);
connect(_checkDevicesTimer, &QTimer::timeout, [this] {
connect(_checkDevicesTimer, &QTimer::timeout, this, [this] {
QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkDevices(); });
});
const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
@ -243,7 +247,7 @@ AudioClient::AudioClient() :
// start a thread to detect peak value changes
_checkPeakValuesTimer = new QTimer(this);
connect(_checkPeakValuesTimer, &QTimer::timeout, [this] {
connect(_checkPeakValuesTimer, &QTimer::timeout, this, [this] {
QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkPeakValues(); });
});
const unsigned long PEAK_VALUES_CHECK_INTERVAL_MSECS = 50;
@ -482,6 +486,15 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice,
audioFormat.setSampleType(QAudioFormat::SignedInt);
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
#if defined(Q_OS_ANDROID)
// Using the HW sample rate (AUDIO_INPUT_FLAG_FAST) in some samsung phones causes a low volume at input stream
// Changing the sample rate forces a resampling that (in samsung) amplifies +18 dB
QAndroidJniObject brand = QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build", "BRAND");
if (audioDevice == QAudioDeviceInfo::defaultInputDevice() && brand.toString().contains("samsung", Qt::CaseInsensitive)) {
audioFormat.setSampleRate(24000);
}
#endif
if (!audioDevice.isFormatSupported(audioFormat)) {
qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed.";
return false;
@ -635,9 +648,7 @@ void AudioClient::start() {
qCDebug(audioclient) << "The closest format available is" << outputDeviceInfo.nearestFormat(_desiredOutputFormat);
}
#if defined(Q_OS_ANDROID)
connect(&_checkInputTimer, &QTimer::timeout, [this] {
checkInputTimeout();
});
connect(&_checkInputTimer, &QTimer::timeout, this, &AudioClient::checkInputTimeout);
_checkInputTimer.start(CHECK_INPUT_READS_MSECS);
#endif
}
@ -651,6 +662,7 @@ void AudioClient::stop() {
switchOutputToAudioDevice(QAudioDeviceInfo(), true);
#if defined(Q_OS_ANDROID)
_checkInputTimer.stop();
disconnect(&_checkInputTimer, &QTimer::timeout, 0, 0);
#endif
}
@ -1558,9 +1570,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInf
#if defined(Q_OS_ANDROID)
if (_audioInput) {
_shouldRestartInputSetup = true;
connect(_audioInput, &QAudioInput::stateChanged, [this](QAudio::State state) {
audioInputStateChanged(state);
});
connect(_audioInput, &QAudioInput::stateChanged, this, &AudioClient::audioInputStateChanged);
}
#endif
_inputDevice = _audioInput->start();
@ -1764,7 +1774,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI
_outputScratchBuffer = new int16_t[_outputPeriod];
// size local output mix buffer based on resampled network frame size
int networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO;
_localOutputMixBuffer = new float[networkPeriod];
// local period should be at least twice the output period,

View file

@ -48,6 +48,13 @@ void HTTPManager::incomingConnection(qintptr socketDescriptor) {
}
bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
// Reject paths with embedded NULs
if (url.path().contains(QChar(0x00))) {
connection->respond(HTTPConnection::StatusCode400, "Embedded NULs not allowed in requests");
qCWarning(embeddedwebserver) << "Received a request with embedded NULs";
return true;
}
if (!skipSubHandler && requestHandledByRequestHandler(connection, url)) {
// this request was handled by our request handler object
// so we don't need to attempt to do so in the document root
@ -57,17 +64,27 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url,
if (!_documentRoot.isEmpty()) {
// check to see if there is a file to serve from the document root for this path
QString subPath = url.path();
// remove any slash at the beginning of the path
if (subPath.startsWith('/')) {
subPath.remove(0, 1);
}
QString absoluteDocumentRoot { QFileInfo(_documentRoot).absolutePath() };
QString filePath;
if (QFileInfo(_documentRoot + subPath).isFile()) {
filePath = _documentRoot + subPath;
} else if (subPath.size() > 0 && !subPath.endsWith('/')) {
QFileInfo pathFileInfo { _documentRoot + subPath };
QString absoluteFilePath { pathFileInfo.absoluteFilePath() };
// The absolute path for this file isn't under the document root
if (absoluteFilePath.indexOf(absoluteDocumentRoot) != 0) {
qCWarning(embeddedwebserver) << absoluteFilePath << "is outside the document root";
connection->respond(HTTPConnection::StatusCode400, "Requested path outside document root");
return true;
}
if (pathFileInfo.isFile()) {
filePath = absoluteFilePath;
} else if (subPath.size() > 0 && !subPath.endsWith('/') && pathFileInfo.isDir()) {
// this could be a directory with a trailing slash
// send a redirect to the path with a slash so we can
QString redirectLocation = '/' + subPath + '/';
@ -80,6 +97,7 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url,
redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8());
connection->respond(HTTPConnection::StatusCode302, "", HTTPConnection::DefaultContentType, redirectHeader);
return true;
}
// if the last thing is a trailing slash then we want to look for index file
@ -87,8 +105,8 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url,
QStringList possibleIndexFiles = QStringList() << "index.html" << "index.shtml";
foreach (const QString& possibleIndexFilename, possibleIndexFiles) {
if (QFileInfo(_documentRoot + subPath + possibleIndexFilename).exists()) {
filePath = _documentRoot + subPath + possibleIndexFilename;
if (QFileInfo(absoluteFilePath + possibleIndexFilename).exists()) {
filePath = absoluteFilePath + possibleIndexFilename;
break;
}
}

View file

@ -185,7 +185,7 @@ private:
#if defined(Q_OS_ANDROID)
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false };
#else
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false }; // False, until such time as it is made to work better.
#endif
#if (PR_BUILD || DEV_BUILD)

View file

@ -229,7 +229,7 @@ qint64 Socket::writeDatagram(const QByteArray& datagram, const HifiSockAddr& soc
if (bytesWritten < 0) {
// when saturating a link this isn't an uncommon message - suppress it so it doesn't bomb the debug
HIFI_FCDEBUG(networking(), "Socket::writeDatagram" << _udpSocket.error() << "-" << qPrintable(_udpSocket.errorString()) );
HIFI_FCDEBUG(networking(), "Socket::writeDatagram" << _udpSocket.error());
}
return bytesWritten;
@ -513,7 +513,7 @@ std::vector<HifiSockAddr> Socket::getConnectionSockAddrs() {
}
void Socket::handleSocketError(QAbstractSocket::SocketError socketError) {
HIFI_FCDEBUG(networking(), "udt::Socket error - " << socketError << _udpSocket.errorString());
HIFI_FCDEBUG(networking(), "udt::Socket error - " << socketError);
}
void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) {

187
scripts/modules/appUi.js Normal file
View file

@ -0,0 +1,187 @@
"use strict";
/*global Tablet, Script*/
//
// libraries/appUi.js
//
// Created by Howard Stearns on 3/20/18.
// 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) {
/* 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.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen.
return (type === that.type) && that.currentUrl && (tabletUrl.indexOf(that.currentUrl) >= 0); // Actual url may have prefix or suffix.
};
that.setCurrentData = function setCurrentData(url) {
that.currentUrl = url;
that.type = /.qml$/.test(url) ? 'QML' : 'Web';
}
that.open = function open(optionalUrl) { // How to open the app.
var url = optionalUrl || that.home;
that.setCurrentData(url);
if (that.isQML()) {
that.tablet.loadQMLSource(url);
} else {
that.tablet.gotoWebScreen(url, that.inject);
}
};
that.close = function close() { // How to close the app.
that.currentUrl = "";
// 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.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.isQML = function isQML() { // We set type property in onClick.
return that.type === 'QML';
};
that.eventSignal = function eventSignal() { // What signal to hook onMessage to.
return that.isQML() ? that.tablet.fromQml : that.tablet.webEventReceived;
};
// Overwrite with the given properties:
Object.keys(properties).forEach(function (key) { that[key] = properties[key]; });
// 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');
that.button = that.tablet.addButton({
icon: that.normalButton,
activeIcon: that.activeButton,
text: that.buttonName,
sortOrder: that.sortOrder
});
that.ignore = function ignore() { };
// Handlers
that.onScreenChanged = function onScreenChanged(type, url) {
// Set isOpen, wireEventBridge, set buttonActive as appropriate,
// and finally call onOpened() or onClosed() IFF defined.
console.debug(that.buttonName, 'onScreenChanged', type, url, that.isOpen);
if (that.checkIsOpen(type, url)) {
if (!that.isOpen) {
that.wireEventBridge(true);
that.buttonActive(true);
if (that.onOpened) {
that.onOpened();
}
that.isOpen = true;
}
} else { // Not us. Should we do something for type Home, Menu, and particularly Closed (meaning tablet hidden?
if (that.isOpen) {
that.wireEventBridge(false);
that.buttonActive(false);
if (that.onClosed) {
that.onClosed();
}
that.isOpen = false;
}
}
};
that.hasEventBridge = 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) { that.onMessage(JSON.parse(messageString)); };
that.wireEventBridge = function wireEventBridge(on) {
// Uniquivocally sets that.sendMessage(messageObject) to do the right thing.
// Sets hasEventBridge and wires onMessage to eventSignal as appropriate, IFF onMessage defined.
var handler, isQml = that.isQML();
// Outbound (always, regardless of whether there is an inbound handler).
if (on) {
that.sendMessage = isQml ? that.tablet.sendToQml : that.sendToHtml;
} else {
that.sendMessage = that.ignore;
}
if (!that.onMessage) { return; }
// Inbound
handler = isQml ? that.onMessage : that.fromHtml;
if (on) {
if (!that.hasEventBridge) {
console.debug(that.buttonName, 'connecting', that.eventSignal());
that.eventSignal().connect(handler);
that.hasEventBridge = true;
}
} else {
if (that.hasEventBridge) {
console.debug(that.buttonName, 'disconnecting', that.eventSignal());
that.eventSignal().disconnect(handler);
that.hasEventBridge = 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.
if (that.isOpen) {
that.close();
}
that.tablet.screenChanged.disconnect(that.onScreenChanged);
if (that.button) {
if (that.onClicked) {
that.button.clicked.disconnect(that.onClicked);
}
that.tablet.removeButton(that.button);
}
};
// Set up the handlers.
that.tablet.screenChanged.connect(that.onScreenChanged);
that.button.clicked.connect(that.onClicked);
Script.scriptEnding.connect(that.onScriptEnding);
}
module.exports = AppUi;

View file

@ -29,13 +29,25 @@ function executeLater(callback) {
Script.setTimeout(callback, 300);
}
function getMyAvatarWearables() {
var wearablesArray = MyAvatar.getAvatarEntitiesVariant();
var INVALID_JOINT_INDEX = -1
function isWearable(avatarEntity) {
return avatarEntity.properties.visible === true && avatarEntity.properties.parentJointIndex !== INVALID_JOINT_INDEX &&
(avatarEntity.properties.parentID === MyAvatar.sessionUUID || avatarEntity.properties.parentID === MyAvatar.SELF_ID);
}
for(var i = 0; i < wearablesArray.length; ++i) {
var wearable = wearablesArray[i];
var localRotation = wearable.properties.localRotation;
wearable.properties.localRotationAngles = Quat.safeEulerAngles(localRotation)
function getMyAvatarWearables() {
var entitiesArray = MyAvatar.getAvatarEntitiesVariant();
var wearablesArray = [];
for (var i = 0; i < entitiesArray.length; ++i) {
var entity = entitiesArray[i];
if (!isWearable(entity)) {
continue;
}
var localRotation = entity.properties.localRotation;
entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation)
wearablesArray.push(entity);
}
return wearablesArray;
@ -159,13 +171,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
for(var bookmarkName in message.data.bookmarks) {
var bookmark = message.data.bookmarks[bookmarkName];
if (!bookmark.avatarEntites) { // ensure avatarEntites always exist
bookmark.avatarEntites = [];
}
bookmark.avatarEntites.forEach(function(avatarEntity) {
avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation)
})
if (bookmark.avatarEntites) {
bookmark.avatarEntites.forEach(function(avatarEntity) {
avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation);
});
}
}
sendToQml(message)
@ -444,10 +455,6 @@ startup();
var isWired = false;
function off() {
if (isWired) { // It is not ok to disconnect these twice, hence guard.
isWired = false;
}
if(adjustWearables.opened) {
adjustWearables.setOpened(false);
ensureWearableSelected(null);
@ -457,16 +464,20 @@ function off() {
Messages.unsubscribe('Hifi-Object-Manipulation');
}
AvatarBookmarks.bookmarkLoaded.disconnect(onBookmarkLoaded);
AvatarBookmarks.bookmarkDeleted.disconnect(onBookmarkDeleted);
AvatarBookmarks.bookmarkAdded.disconnect(onBookmarkAdded);
if (isWired) { // It is not ok to disconnect these twice, hence guard.
isWired = false;
MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged);
AvatarBookmarks.bookmarkLoaded.disconnect(onBookmarkLoaded);
AvatarBookmarks.bookmarkDeleted.disconnect(onBookmarkDeleted);
AvatarBookmarks.bookmarkAdded.disconnect(onBookmarkAdded);
MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged);
}
}
function on() {

View file

@ -10,7 +10,7 @@
/* global Script, Controller, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
Messages, makeDispatcherModuleParameters, HMD, getGrabPointSphereOffset, COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
COLORS_GRAB_SEARCHING_FULL_SQUEEZE, COLORS_GRAB_DISTANCE_HOLD, DEFAULT_SEARCH_SPHERE_DISTANCE, TRIGGER_ON_VALUE,
getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, Picks, makeLaserParams
getEnabledModuleByName, PICK_MAX_DISTANCE, isInEditMode, Picks, makeLaserParams, Entities
*/
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@ -19,7 +19,7 @@ Script.include("/~/system/libraries/utils.js");
(function () {
var MARGIN = 25;
var TABLET_MATERIAL_ENTITY_NAME = 'Tablet-Material-Entity';
function InEditMode(hand) {
this.hand = hand;
this.triggerClicked = false;
@ -53,7 +53,7 @@ Script.include("/~/system/libraries/utils.js");
return (HMD.tabletScreenID && objectID === HMD.tabletScreenID)
|| (HMD.homeButtonID && objectID === HMD.homeButtonID);
};
this.calculateNewReticlePosition = function(intersection) {
var dims = Controller.getViewportDimensions();
this.reticleMaxX = dims.x - MARGIN;
@ -75,10 +75,12 @@ Script.include("/~/system/libraries/utils.js");
}
}
if (this.selectedTarget.type === Picks.INTERSECTED_ENTITY) {
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
method: "selectEntity",
entityID: this.selectedTarget.objectID
}));
if (!this.isTabletMaterialEntity(this.selectedTarget.objectID)) {
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
method: "selectEntity",
entityID: this.selectedTarget.objectID
}));
}
} else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) {
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
method: "selectOverlay",
@ -88,10 +90,16 @@ Script.include("/~/system/libraries/utils.js");
this.triggerClicked = true;
}
this.sendPointingAtData(controllerData);
};
this.isTabletMaterialEntity = function(entityID) {
return ((entityID === HMD.homeButtonHighlightMaterialID) ||
(entityID === HMD.homeButtonUnhighlightMaterialID));
};
this.sendPointingAtData = function(controllerData) {
var rayPick = controllerData.rayPicks[this.hand];
var hudRayPick = controllerData.hudRayPicks[this.hand];

View file

@ -77,6 +77,8 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = {
dimensions: viveNaturalDimensions,
parts: {
// DISABLED FOR NOW
/*
tips: {
type: "static",
modelURL: viveTipsModelURL,
@ -103,6 +105,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = {
}
}
},
*/
// The touchpad type draws a dot indicating the current touch/thumb position
// and swaps in textures based on the thumb position.
@ -215,6 +218,8 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = {
},
parts: {
// DISABLED FOR NOW
/*
tips: {
type: "static",
modelURL: viveTipsModelURL,
@ -242,6 +247,7 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = {
}
}
},
*/
// The touchpad type draws a dot indicating the current touch/thumb position
// and swaps in textures based on the thumb position.

View file

@ -1922,7 +1922,11 @@ function applyEntityProperties(data) {
Entities.deleteEntity(entityID);
}
selectionManager.setSelections(selectedEntityIDs);
// We might be getting an undo while edit.js is disabled. If that is the case, don't set
// our selections, causing the edit widgets to display.
if (isActive) {
selectionManager.setSelections(selectedEntityIDs);
}
}
// For currently selected entities, push a command to the UndoStack that uses the current entity properties for the

View file

@ -30,6 +30,8 @@ var INCHES_TO_METERS = 1 / 39.3701;
var NO_HANDS = -1;
var DELAY_FOR_30HZ = 33; // milliseconds
var TABLET_MATERIAL_ENTITY_NAME = 'Tablet-Material-Entity';
// will need to be recaclulated if dimensions of fbx model change.
var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269};
@ -79,6 +81,19 @@ function calcSpawnInfo(hand, landscape) {
};
}
cleanUpOldMaterialEntities = function() {
var avatarEntityData = MyAvatar.getAvatarEntityData();
for (var entityID in avatarEntityData) {
var entityName = Entities.getEntityProperties(entityID, ["name"]).name;
if (entityName === TABLET_MATERIAL_ENTITY_NAME && entityID !== HMD.homeButtonHighlightMaterialID &&
entityID !== HMD.homeButtonUnhighlightMaterialID) {
Entities.deleteEntity(entityID);
}
}
};
/**
* WebTablet
* @param url [string] url of content to show on the tablet.
@ -134,6 +149,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) {
}
this.cleanUpOldTablets();
cleanUpOldMaterialEntities();
this.tabletEntityID = Overlays.addOverlay("model", tabletProperties);
@ -180,6 +196,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) {
this.homeButtonUnhighlightMaterial = Entities.addEntity({
type: "Material",
name: TABLET_MATERIAL_ENTITY_NAME,
materialURL: "materialData",
localPosition: { x: 0.0, y: 0.0, z: 0.0 },
priority: HIGH_PRIORITY,
@ -199,6 +216,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) {
this.homeButtonHighlightMaterial = Entities.addEntity({
type: "Material",
name: TABLET_MATERIAL_ENTITY_NAME,
materialURL: "materialData",
localPosition: { x: 0.0, y: 0.0, z: 0.0 },
priority: LOW_PRIORITY,

View file

@ -111,6 +111,11 @@ EntityListTool = function(shouldUseEditTabletApp) {
return value !== undefined ? value : "";
}
function filterEntity(entityID) {
return ((entityID === HMD.homeButtonHighlightMaterialID) ||
(entityID === HMD.homeButtonUnhighlightMaterialID));
}
that.sendUpdate = function() {
var entities = [];
@ -121,6 +126,10 @@ EntityListTool = function(shouldUseEditTabletApp) {
ids = Entities.findEntities(MyAvatar.position, searchRadius);
}
ids = ids.filter(function(id) {
return !filterEntity(id);
});
var cameraPosition = Camera.position;
for (var i = 0; i < ids.length; i++) {
var id = ids[i];

View file

@ -263,7 +263,7 @@ SelectionManager = (function() {
that.worldDimensions = properties.boundingBox.dimensions;
that.worldPosition = properties.boundingBox.center;
that.worldRotation = properties.boundingBox.rotation;
that.worldRotation = Quat.IDENTITY;
that.entityType = properties.type;
@ -271,10 +271,6 @@ SelectionManager = (function() {
SelectionDisplay.setSpaceMode(SPACE_LOCAL);
}
} else {
that.localRotation = null;
that.localDimensions = null;
that.localPosition = null;
properties = Entities.getEntityProperties(that.selections[0]);
that.entityType = properties.type;
@ -293,6 +289,7 @@ SelectionManager = (function() {
tfl.z = Math.max(bb.tfl.z, tfl.z);
}
that.localRotation = null;
that.localDimensions = null;
that.localPosition = null;
that.worldDimensions = {
@ -300,6 +297,7 @@ SelectionManager = (function() {
y: tfl.y - brn.y,
z: tfl.z - brn.z
};
that.worldRotation = Quat.IDENTITY;
that.worldPosition = {
x: brn.x + (that.worldDimensions.x / 2),
y: brn.y + (that.worldDimensions.y / 2),
@ -381,6 +379,8 @@ SelectionDisplay = (function() {
var CTRL_KEY_CODE = 16777249;
var RAIL_AXIS_LENGTH = 10000;
var TRANSLATE_DIRECTION = {
X: 0,
Y: 1,
@ -616,6 +616,40 @@ SelectionDisplay = (function() {
dashed: false
});
var xRailOverlay = Overlays.addOverlay("line3d", {
visible: false,
start: Vec3.ZERO,
end: Vec3.ZERO,
color: {
red: 255,
green: 0,
blue: 0
},
ignoreRayIntersection: true // always ignore this
});
var yRailOverlay = Overlays.addOverlay("line3d", {
visible: false,
start: Vec3.ZERO,
end: Vec3.ZERO,
color: {
red: 0,
green: 255,
blue: 0
},
ignoreRayIntersection: true // always ignore this
});
var zRailOverlay = Overlays.addOverlay("line3d", {
visible: false,
start: Vec3.ZERO,
end: Vec3.ZERO,
color: {
red: 0,
green: 0,
blue: 255
},
ignoreRayIntersection: true // always ignore this
});
var allOverlays = [
handleTranslateXCone,
handleTranslateXCylinder,
@ -656,7 +690,11 @@ SelectionDisplay = (function() {
handleScaleFLEdge,
handleCloner,
selectionBox,
iconSelectionBox
iconSelectionBox,
xRailOverlay,
yRailOverlay,
zRailOverlay
];
var maximumHandleInAllOverlays = handleCloner;
@ -873,11 +911,13 @@ SelectionDisplay = (function() {
};
// FUNCTION: MOUSE MOVE EVENT
var lastMouseEvent = null;
that.mouseMoveEvent = function(event) {
var wantDebug = false;
if (wantDebug) {
print("=============== eST::MouseMoveEvent BEG =======================");
}
lastMouseEvent = event;
if (activeTool) {
if (wantDebug) {
print(" Trigger ActiveTool(" + activeTool.mode + ")'s onMove");
@ -999,19 +1039,35 @@ SelectionDisplay = (function() {
};
// Control key remains active only while key is held down
that.keyReleaseEvent = function(key) {
if (key.key === CTRL_KEY_CODE) {
that.keyReleaseEvent = function(event) {
if (event.key === CTRL_KEY_CODE) {
ctrlPressed = false;
that.updateActiveRotateRing();
}
if (activeTool && lastMouseEvent !== null) {
lastMouseEvent.isShifted = event.isShifted;
lastMouseEvent.isMeta = event.isMeta;
lastMouseEvent.isControl = event.isControl;
lastMouseEvent.isAlt = event.isAlt;
activeTool.onMove(lastMouseEvent);
SelectionManager._update();
}
};
// Triggers notification on specific key driven events
that.keyPressEvent = function(key) {
if (key.key === CTRL_KEY_CODE) {
that.keyPressEvent = function(event) {
if (event.key === CTRL_KEY_CODE) {
ctrlPressed = true;
that.updateActiveRotateRing();
}
if (activeTool && lastMouseEvent !== null) {
lastMouseEvent.isShifted = event.isShifted;
lastMouseEvent.isMeta = event.isMeta;
lastMouseEvent.isControl = event.isControl;
lastMouseEvent.isAlt = event.isAlt;
activeTool.onMove(lastMouseEvent);
SelectionManager._update();
}
};
// NOTE: mousePressEvent and mouseMoveEvent from the main script should call us., so we don't hook these:
@ -1705,6 +1761,14 @@ SelectionDisplay = (function() {
},
onEnd: function(event, reason) {
pushCommandForSelections(duplicatedEntityIDs);
if (isConstrained) {
Overlays.editOverlay(xRailOverlay, {
visible: false
});
Overlays.editOverlay(zRailOverlay, {
visible: false
});
}
},
elevation: function(origin, intersection) {
return (origin.y - intersection.y) / Vec3.distance(origin, intersection);
@ -1768,10 +1832,46 @@ SelectionDisplay = (function() {
vector.x = 0;
}
if (!isConstrained) {
var xStart = Vec3.sum(startPosition, {
x: -RAIL_AXIS_LENGTH,
y: 0,
z: 0
});
var xEnd = Vec3.sum(startPosition, {
x: RAIL_AXIS_LENGTH,
y: 0,
z: 0
});
var zStart = Vec3.sum(startPosition, {
x: 0,
y: 0,
z: -RAIL_AXIS_LENGTH
});
var zEnd = Vec3.sum(startPosition, {
x: 0,
y: 0,
z: RAIL_AXIS_LENGTH
});
Overlays.editOverlay(xRailOverlay, {
start: xStart,
end: xEnd,
visible: true
});
Overlays.editOverlay(zRailOverlay, {
start: zStart,
end: zEnd,
visible: true
});
isConstrained = true;
}
} else {
if (isConstrained) {
Overlays.editOverlay(xRailOverlay, {
visible: false
});
Overlays.editOverlay(zRailOverlay, {
visible: false
});
isConstrained = false;
}
}

View file

@ -12,14 +12,15 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function() { // BEGIN LOCAL_SCOPE
(function () { // BEGIN LOCAL_SCOPE
var request = Script.require('request').request;
var request = Script.require('request').request;
var AppUi = Script.require('appUi');
var populateNearbyUserList, color, textures, removeOverlays,
controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged,
controllerComputePickRay, off,
receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL,
tablet, CHANNEL, getConnectionData, findableByChanged,
CHANNEL, getConnectionData, findableByChanged,
avatarAdded, avatarRemoved, avatarSessionChanged; // forward references;
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed
@ -40,6 +41,7 @@ var HOVER_TEXTURES = {
var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6};
var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29};
var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now
var METAVERSE_BASE = Account.metaverseServerURL;
Script.include("/~/system/libraries/controllers.js");
@ -221,7 +223,7 @@ function convertDbToLinear(decibels) {
return Math.pow(2, decibels / 10.0);
}
function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
var data;
var data, connectionUserName, friendUserName;
switch (message.method) {
case 'selected':
selectedIds = message.params;
@ -281,7 +283,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
}
getConnectionData(false);
});
break
break;
case 'removeFriend':
friendUserName = message.params;
@ -296,7 +298,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
}
getConnectionData(friendUserName);
});
break
break;
case 'addFriend':
friendUserName = message.params;
print("Adding " + friendUserName + " to friends.");
@ -307,24 +309,23 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
body: {
username: friendUserName,
}
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to friend " + friendUserName, error || response.status);
return;
}
getConnectionData(friendUserName);
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to friend " + friendUserName, error || response.status);
return;
}
);
getConnectionData(friendUserName);
});
break;
case 'http.request':
break; // Handled by request-service.
break; // Handled by request-service.
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
}
}
function sendToQml(message) {
tablet.sendToQml(message);
ui.sendMessage(message);
}
function updateUser(data) {
print('PAL update:', JSON.stringify(data));
@ -334,7 +335,6 @@ function updateUser(data) {
// User management services
//
// These are prototype versions that will be changed when the back end changes.
var METAVERSE_BASE = Account.metaverseServerURL;
function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise.
request({
@ -362,7 +362,7 @@ function getProfilePicture(username, callback) { // callback(url) if successfull
});
}
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
url = METAVERSE_BASE + '/api/v1/users?per_page=400&'
var url = METAVERSE_BASE + '/api/v1/users?per_page=400&';
if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces
} else {
@ -373,7 +373,7 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca
});
}
function getInfoAboutUser(specificUsername, callback) {
url = METAVERSE_BASE + '/api/v1/users?filter=connections'
var url = METAVERSE_BASE + '/api/v1/users?filter=connections';
requestJSON(url, function (connectionsData) {
for (user in connectionsData.users) {
if (connectionsData.users[user].username === specificUsername) {
@ -705,77 +705,41 @@ triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Cont
triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
var ui;
// Most apps can have people toggle the tablet closed and open again, and the app should remain "open" even while
// the tablet is not shown. However, for the pal, we explicitly close the app and return the tablet to it's
// home screen (so that the avatar highlighting goes away).
function tabletVisibilityChanged() {
if (!tablet.tabletShown && onPalScreen) {
ContextOverlay.enabled = true;
tablet.gotoHomeScreen();
}
}
var wasOnPalScreen = false;
var onPalScreen = false;
var PAL_QML_SOURCE = "hifi/Pal.qml";
function onTabletButtonClicked() {
if (!tablet) {
print("Warning in onTabletButtonClicked(): 'tablet' undefined!");
return;
}
if (onPalScreen) {
// In Toolbar Mode, `gotoHomeScreen` will close the app window.
tablet.gotoHomeScreen();
} else {
tablet.loadQMLSource(PAL_QML_SOURCE);
}
}
var hasEventBridge = false;
function wireEventBridge(on) {
if (on) {
if (!hasEventBridge) {
tablet.fromQml.connect(fromQml);
hasEventBridge = true;
}
} else {
if (hasEventBridge) {
tablet.fromQml.disconnect(fromQml);
hasEventBridge = false;
}
if (!ui.tablet.tabletShown && ui.isOpen) {
ui.close();
}
}
var UPDATE_INTERVAL_MS = 100;
var updateInterval;
function createUpdateInterval() {
return Script.setInterval(function () {
updateOverlays();
}, UPDATE_INTERVAL_MS);
}
function onTabletScreenChanged(type, url) {
wasOnPalScreen = onPalScreen;
onPalScreen = (type === "QML" && url === PAL_QML_SOURCE);
wireEventBridge(onPalScreen);
// for toolbar mode: change button to active when window is first openend, false otherwise.
button.editProperties({isActive: onPalScreen});
var previousContextOverlay = ContextOverlay.enabled;
var previousRequestsDomainListData = Users.requestsDomainListData;
function on() {
if (onPalScreen) {
isWired = true;
previousContextOverlay = ContextOverlay.enabled;
previousRequestsDomainListData = Users.requestsDomainListData
ContextOverlay.enabled = false;
Users.requestsDomainListData = true;
ContextOverlay.enabled = false;
Users.requestsDomainListData = true;
tablet.tabletShownChanged.connect(tabletVisibilityChanged);
updateInterval = createUpdateInterval();
Controller.mousePressEvent.connect(handleMouseEvent);
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
Users.usernameFromIDReply.connect(usernameFromIDReply);
triggerMapping.enable();
triggerPressMapping.enable();
populateNearbyUserList();
} else {
off();
if (wasOnPalScreen) {
ContextOverlay.enabled = true;
}
}
ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged);
updateInterval = createUpdateInterval();
Controller.mousePressEvent.connect(handleMouseEvent);
Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
Users.usernameFromIDReply.connect(usernameFromIDReply);
triggerMapping.enable();
triggerPressMapping.enable();
populateNearbyUserList();
}
//
@ -789,8 +753,8 @@ function receiveMessage(channel, messageString, senderID) {
var message = JSON.parse(messageString);
switch (message.method) {
case 'select':
if (!onPalScreen) {
tablet.loadQMLSource(PAL_QML_SOURCE);
if (!ui.isOpen) {
ui.open();
Script.setTimeout(function () { sendToQml(message); }, 1000);
} else {
sendToQml(message); // Accepts objects, not just strings.
@ -826,9 +790,8 @@ function avatarDisconnected(nodeID) {
function clearLocalQMLDataAndClosePAL() {
sendToQml({ method: 'clearLocalQMLData' });
if (onPalScreen) {
ContextOverlay.enabled = true;
tablet.gotoHomeScreen();
if (ui.isOpen) {
ui.close();
}
}
@ -844,20 +807,15 @@ function avatarSessionChanged(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] });
}
var button;
var buttonName = "PEOPLE";
var tablet = null;
function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
button = tablet.addButton({
text: buttonName,
icon: "icons/tablet-icons/people-i.svg",
activeIcon: "icons/tablet-icons/people-a.svg",
sortOrder: 7
ui = new AppUi({
buttonName: "PEOPLE",
sortOrder: 7,
home: "hifi/Pal.qml",
onOpened: on,
onClosed: off,
onMessage: fromQml
});
button.clicked.connect(onTabletButtonClicked);
tablet.screenChanged.connect(onTabletScreenChanged);
Window.domainChanged.connect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL);
@ -869,35 +827,25 @@ function startup() {
}
startup();
var isWired = false;
function off() {
if (isWired) {
if (ui.isOpen) { // i.e., only when connected
if (updateInterval) {
Script.clearInterval(updateInterval);
}
Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
ContextOverlay.enabled = true
triggerMapping.disable();
triggerPressMapping.disable();
Users.requestsDomainListData = false;
isWired = false;
}
removeOverlays();
ContextOverlay.enabled = previousContextOverlay;
Users.requestsDomainListData = previousRequestsDomainListData;
}
function shutdown() {
if (onPalScreen) {
tablet.gotoHomeScreen();
}
button.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(button);
tablet.screenChanged.disconnect(onTabletScreenChanged);
Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL);
Messages.subscribe(CHANNEL);

View file

@ -13,7 +13,7 @@
//
/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays,
MyAvatar, Menu, AvatarInputs, Vec3 */
MyAvatar, Menu, AvatarInputs, Vec3, cleanUpOldMaterialEntities */
(function() { // BEGIN LOCAL_SCOPE
var tabletRezzed = false;
@ -31,6 +31,14 @@
Script.include("../libraries/WebTablet.js");
function cleanupMaterialEntities() {
if (Window.isPhysicsEnabled()) {
cleanUpOldMaterialEntities();
return;
}
Script.setTimeout(cleanupMaterialEntities, 100);
}
function checkTablet() {
if (gTablet === null) {
gTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
@ -327,4 +335,5 @@
HMD.homeButtonHighlightMaterialID = null;
HMD.homeButtonUnhighlightMaterialID = null;
});
Script.setTimeout(cleanupMaterialEntities, 100);
}()); // END LOCAL_SCOPE

View file

@ -405,7 +405,7 @@ LogWindow.prototype = {
}
};
function goHomeClicked() {
function visitSandboxClicked() {
if (interfacePath) {
startInterface('hifi://localhost');
} else {
@ -439,8 +439,8 @@ var labels = {
}
},
goHome: {
label: 'Go Home',
click: goHomeClicked,
label: 'Visit Sandbox',
click: visitSandboxClicked,
enabled: false
},
quit: {

View file

@ -5,7 +5,7 @@ project(${TARGET_NAME})
SET (CMAKE_AUTOUIC ON)
SET (CMAKE_AUTOMOC ON)
setup_hifi_project (Core Widgets Network)
setup_hifi_project (Core Widgets Network Xml)
link_hifi_libraries ()
# FIX: Qt was built with -reduce-relocations
@ -18,7 +18,7 @@ include_directories (${CMAKE_CURRENT_SOURCE_DIR})
include_directories (${Qt5Core_INCLUDE_DIRS})
include_directories (${Qt5Widgets_INCLUDE_DIRS})
set (QT_LIBRARIES Qt5::Core Qt5::Widgets)
set (QT_LIBRARIES Qt5::Core Qt5::Widgets QT::Gui Qt5::Xml)
if (WIN32)
# Do not show Console

View file

@ -24,7 +24,7 @@ extern AutoTester* autoTester;
#include <math.h>
Test::Test() {
mismatchWindow.setModal(true);
_mismatchWindow.setModal(true);
if (autoTester) {
autoTester->setUserText("highfidelity");
@ -34,35 +34,35 @@ Test::Test() {
bool Test::createTestResultsFolderPath(const QString& directory) {
QDateTime now = QDateTime::currentDateTime();
testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT);
QDir testResultsFolder(testResultsFolderPath);
_testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT);
QDir testResultsFolder(_testResultsFolderPath);
// Create a new test results folder
return QDir().mkdir(testResultsFolderPath);
return QDir().mkdir(_testResultsFolderPath);
}
void Test::zipAndDeleteTestResultsFolder() {
QString zippedResultsFileName { testResultsFolderPath + ".zip" };
QString zippedResultsFileName { _testResultsFolderPath + ".zip" };
QFileInfo fileInfo(zippedResultsFileName);
if (!fileInfo.exists()) {
QFile::remove(zippedResultsFileName);
}
QDir testResultsFolder(testResultsFolderPath);
QDir testResultsFolder(_testResultsFolderPath);
if (!testResultsFolder.isEmpty()) {
JlCompress::compressDir(testResultsFolderPath + ".zip", testResultsFolderPath);
JlCompress::compressDir(_testResultsFolderPath + ".zip", _testResultsFolderPath);
}
testResultsFolder.removeRecursively();
//In all cases, for the next evaluation
testResultsFolderPath = "";
index = 1;
_testResultsFolderPath = "";
_index = 1;
}
bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) {
progressBar->setMinimum(0);
progressBar->setMaximum(expectedImagesFullFilenames.length() - 1);
progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1);
progressBar->setValue(0);
progressBar->setVisible(true);
@ -70,10 +70,10 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar)
// Quit loop if user has aborted due to a failed test.
bool success{ true };
bool keepOn{ true };
for (int i = 0; keepOn && i < expectedImagesFullFilenames.length(); ++i) {
for (int i = 0; keepOn && i < _expectedImagesFullFilenames.length(); ++i) {
// First check that images are the same size
QImage resultImage(resultImagesFullFilenames[i]);
QImage expectedImage(expectedImagesFullFilenames[i]);
QImage resultImage(_resultImagesFullFilenames[i]);
QImage expectedImage(_expectedImagesFullFilenames[i]);
double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical
@ -82,30 +82,30 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar)
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size");
similarityIndex = -100.0;
} else {
similarityIndex = imageComparer.compareImages(resultImage, expectedImage);
similarityIndex = _imageComparer.compareImages(resultImage, expectedImage);
}
if (similarityIndex < THRESHOLD) {
TestFailure testFailure = TestFailure{
(float)similarityIndex,
expectedImagesFullFilenames[i].left(expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /)
QFileInfo(expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image
QFileInfo(resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image
_expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /)
QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image
QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image
};
mismatchWindow.setTestFailure(testFailure);
_mismatchWindow.setTestFailure(testFailure);
if (!isInteractiveMode) {
appendTestResultsToFile(testResultsFolderPath, testFailure, mismatchWindow.getComparisonImage());
appendTestResultsToFile(_testResultsFolderPath, testFailure, _mismatchWindow.getComparisonImage());
success = false;
} else {
mismatchWindow.exec();
_mismatchWindow.exec();
switch (mismatchWindow.getUserResponse()) {
switch (_mismatchWindow.getUserResponse()) {
case USER_RESPONSE_PASS:
break;
case USE_RESPONSE_FAIL:
appendTestResultsToFile(testResultsFolderPath, testFailure, mismatchWindow.getComparisonImage());
appendTestResultsToFile(_testResultsFolderPath, testFailure, _mismatchWindow.getComparisonImage());
success = false;
break;
case USER_RESPONSE_ABORT:
@ -126,20 +126,20 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar)
return success;
}
void Test::appendTestResultsToFile(const QString& testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) {
if (!QDir().exists(testResultsFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + testResultsFolderPath + " not found");
void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) {
if (!QDir().exists(_testResultsFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found");
exit(-1);
}
QString err = QString::number(testFailure._error).left(6);
QString failureFolderPath { testResultsFolderPath + "/" + err + "-Failure_" + QString::number(index) + "--" + testFailure._actualImageFilename.left(testFailure._actualImageFilename.length() - 4) };
QString failureFolderPath { _testResultsFolderPath + "/" + err + "-Failure_" + QString::number(_index) + "--" + testFailure._actualImageFilename.left(testFailure._actualImageFilename.length() - 4) };
if (!QDir().mkdir(failureFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create folder " + failureFolderPath);
exit(-1);
}
++index;
++_index;
QFile descriptionFile(failureFolderPath + "/" + TEST_RESULTS_FILENAME);
if (!descriptionFile.open(QIODevice::ReadWrite)) {
@ -152,7 +152,7 @@ void Test::appendTestResultsToFile(const QString& testResultsFolderPath, TestFai
stream << "Test failed in folder " << testFailure._pathname.left(testFailure._pathname.length() - 1) << endl; // remove trailing '/'
stream << "Expected image was " << testFailure._expectedImageFilename << endl;
stream << "Actual image was " << testFailure._actualImageFilename << endl;
stream << "Similarity index was " << testFailure._error << endl;
stream << "Similarity _index was " << testFailure._error << endl;
descriptionFile.close();
@ -180,26 +180,26 @@ void Test::appendTestResultsToFile(const QString& testResultsFolderPath, TestFai
void Test::startTestsEvaluation(const QString& testFolder, const QString& branchFromCommandLine, const QString& userFromCommandLine) {
if (testFolder.isNull()) {
// Get list of JPEG images in folder, sorted by name
QString previousSelection = snapshotDirectory;
QString previousSelection = _snapshotDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (snapshotDirectory == "") {
snapshotDirectory = previousSelection;
if (_snapshotDirectory == "") {
_snapshotDirectory = previousSelection;
return;
}
} else {
snapshotDirectory = testFolder;
exitWhenComplete = true;
_snapshotDirectory = testFolder;
_exitWhenComplete = true;
}
// Quit if test results folder could not be created
if (!createTestResultsFolderPath(snapshotDirectory)) {
if (!createTestResultsFolderPath(_snapshotDirectory)) {
return;
}
@ -207,20 +207,20 @@ void Test::startTestsEvaluation(const QString& testFolder, const QString& branch
// The expected images are represented as a URL to enable download from GitHub
// Images that are in the wrong format are ignored.
QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", snapshotDirectory);
QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", _snapshotDirectory);
QStringList expectedImagesURLs;
resultImagesFullFilenames.clear();
expectedImagesFilenames.clear();
expectedImagesFullFilenames.clear();
_resultImagesFullFilenames.clear();
_expectedImagesFilenames.clear();
_expectedImagesFullFilenames.clear();
QString branch = (branchFromCommandLine.isNull()) ? autoTester->getSelectedBranch() : branchFromCommandLine;
QString user = (userFromCommandLine.isNull()) ? autoTester->getSelectedUser() : userFromCommandLine;
foreach(QString currentFilename, sortedTestResultsFilenames) {
QString fullCurrentFilename = snapshotDirectory + "/" + currentFilename;
QString fullCurrentFilename = _snapshotDirectory + "/" + currentFilename;
if (isInSnapshotFilenameFormat("png", currentFilename)) {
resultImagesFullFilenames << fullCurrentFilename;
_resultImagesFullFilenames << fullCurrentFilename;
QString expectedImagePartialSourceDirectory = getExpectedImagePartialSourceDirectory(currentFilename);
@ -237,12 +237,12 @@ void Test::startTestsEvaluation(const QString& testFolder, const QString& branch
// The image retrieved from GitHub needs a unique name
QString expectedImageFilename = currentFilename.replace("/", "_").replace(".png", "_EI.png");
expectedImagesFilenames << expectedImageFilename;
expectedImagesFullFilenames << snapshotDirectory + "/" + expectedImageFilename;
_expectedImagesFilenames << expectedImageFilename;
_expectedImagesFullFilenames << _snapshotDirectory + "/" + expectedImageFilename;
}
}
autoTester->downloadImages(expectedImagesURLs, snapshotDirectory, expectedImagesFilenames);
autoTester->downloadImages(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames);
}
void Test::finishTestsEvaluation(bool isRunningFromCommandline, bool interactiveMode, QProgressBar* progressBar) {
@ -258,7 +258,7 @@ void Test::finishTestsEvaluation(bool isRunningFromCommandline, bool interactive
zipAndDeleteTestResultsFolder();
if (exitWhenComplete) {
if (_exitWhenComplete) {
exit(0);
}
}
@ -310,46 +310,46 @@ void Test::includeTest(QTextStream& textStream, const QString& testPathname) {
// This script will run all text.js scripts in every applicable sub-folder
void Test::createRecursiveScript() {
// Select folder to start recursing from
QString previousSelection = testDirectory;
QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testDirectory =
_testDirectory =
QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testDirectory == "") {
testDirectory = previousSelection;
if (_testDirectory == "") {
_testDirectory = previousSelection;
return;
}
createRecursiveScript(testDirectory, true);
createRecursiveScript(_testDirectory, true);
}
// This method creates a `testRecursive.js` script in every sub-folder.
void Test::createAllRecursiveScripts() {
// Select folder to start recursing from
QString previousSelection = testsRootDirectory;
QString previousSelection = _testsRootDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the recursive scripts",
_testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the recursive scripts",
parent, QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testsRootDirectory == "") {
testsRootDirectory = previousSelection;
if (_testsRootDirectory == "") {
_testsRootDirectory = previousSelection;
return;
}
createRecursiveScript(testsRootDirectory, false);
createRecursiveScript(_testsRootDirectory, false);
QDirIterator it(testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -477,42 +477,42 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact
void Test::createTests() {
// Rename files sequentially, as ExpectedResult_00000.jpeg, ExpectedResult_00001.jpg and so on
// Any existing expected result images will be deleted
QString previousSelection = snapshotDirectory;
QString previousSelection = _snapshotDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
_snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (snapshotDirectory == "") {
snapshotDirectory = previousSelection;
if (_snapshotDirectory == "") {
_snapshotDirectory = previousSelection;
return;
}
previousSelection = testsRootDirectory;
previousSelection = _testsRootDirectory;
parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select test root folder", parent,
_testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select test root folder", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testsRootDirectory == "") {
testsRootDirectory = previousSelection;
if (_testsRootDirectory == "") {
_testsRootDirectory = previousSelection;
return;
}
QStringList sortedImageFilenames = createListOfAll_imagesInDirectory("png", snapshotDirectory);
QStringList sortedImageFilenames = createListOfAll_imagesInDirectory("png", _snapshotDirectory);
int i = 1;
const int maxImages = pow(10, NUM_DIGITS);
foreach (QString currentFilename, sortedImageFilenames) {
QString fullCurrentFilename = snapshotDirectory + "/" + currentFilename;
QString fullCurrentFilename = _snapshotDirectory + "/" + currentFilename;
if (isInSnapshotFilenameFormat("png", currentFilename)) {
if (i >= maxImages) {
QMessageBox::critical(0, "Error", "More than " + QString::number(maxImages) + " images not supported");
@ -522,17 +522,17 @@ void Test::createTests() {
// Path to test is extracted from the file name
// Example:
// filename is tests.engine.interaction.pointer.laser.distanceScaleEnd.00000.jpg
// path is <testDirectory>/engine/interaction/pointer/laser/distanceScaleEnd
// path is <_testDirectory>/engine/interaction/pointer/laser/distanceScaleEnd
//
// Note: we don't use the first part and the last 2 parts of the filename at this stage
//
QStringList pathParts = currentFilename.split(".");
QString fullNewFileName = testsRootDirectory;
QString fullNewFileName = _testsRootDirectory;
for (int j = 1; j < pathParts.size() - 2; ++j) {
fullNewFileName += "/" + pathParts[j];
}
// The image index is the penultimate component of the path parts (the last being the file extension)
// The image _index is the penultimate component of the path parts (the last being the file extension)
QString newFilename = "ExpectedImage_" + pathParts[pathParts.size() - 2].rightJustified(5, '0') + ".png";
fullNewFileName += "/" + newFilename;
@ -621,51 +621,51 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
// The folder selected must contain a script named "test.js", the file produced is named "test.md"
void Test::createMDFile() {
// Folder selection
QString previousSelection = testDirectory;
QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", parent,
_testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testDirectory == "") {
testDirectory = previousSelection;
if (_testDirectory == "") {
_testDirectory = previousSelection;
return;
}
createMDFile(testDirectory);
createMDFile(_testDirectory);
QMessageBox::information(0, "Success", "MD file has been created");
}
void Test::createAllMDFiles() {
// Select folder to start recursing from
QString previousSelection = testsRootDirectory;
QString previousSelection = _testsRootDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the MD files", parent,
_testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the MD files", parent,
QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testsRootDirectory == "") {
testsRootDirectory = previousSelection;
if (_testsRootDirectory == "") {
_testsRootDirectory = previousSelection;
return;
}
// First test if top-level folder has a test.js file
const QString testPathname{ testsRootDirectory + "/" + TEST_FILENAME };
const QString testPathname{ _testsRootDirectory + "/" + TEST_FILENAME };
QFileInfo fileInfo(testPathname);
if (fileInfo.exists()) {
createMDFile(testsRootDirectory);
createMDFile(_testsRootDirectory);
}
QDirIterator it(testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -685,9 +685,9 @@ void Test::createAllMDFiles() {
QMessageBox::information(0, "Success", "MD files have been created");
}
void Test::createMDFile(const QString& testDirectory) {
void Test::createMDFile(const QString& _testDirectory) {
// Verify folder contains test.js file
QString testFileName(testDirectory + "/" + TEST_FILENAME);
QString testFileName(_testDirectory + "/" + TEST_FILENAME);
QFileInfo testFileInfo(testFileName);
if (!testFileInfo.exists()) {
QMessageBox::critical(0, "Error", "Could not find file: " + TEST_FILENAME);
@ -696,7 +696,7 @@ void Test::createMDFile(const QString& testDirectory) {
ExtractedText testScriptLines = getTestScriptLines(testFileName);
QString mdFilename(testDirectory + "/" + "test.md");
QString mdFilename(_testDirectory + "/" + "test.md");
QFile mdFile(mdFilename);
if (!mdFile.open(QIODevice::WriteOnly)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename);
@ -710,7 +710,7 @@ void Test::createMDFile(const QString& testDirectory) {
stream << "# " << testName << "\n";
// Find the relevant part of the path to the test (i.e. from "tests" down
QString partialPath = extractPathFromTestsDown(testDirectory);
QString partialPath = extractPathFromTestsDown(_testDirectory);
stream << "## Run this script URL: [Manual](./test.js?raw=true) [Auto](./testAuto.js?raw=true)(from menu/Edit/Open and Run scripts from URL...)." << "\n\n";
@ -734,23 +734,23 @@ void Test::createMDFile(const QString& testDirectory) {
}
void Test::createTestsOutline() {
QString previousSelection = testDirectory;
QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
testDirectory =
_testDirectory =
QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", parent, QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (testDirectory == "") {
testDirectory = previousSelection;
if (_testDirectory == "") {
_testDirectory = previousSelection;
return;
}
const QString testsOutlineFilename { "testsOutline.md" };
QString mdFilename(testDirectory + "/" + testsOutlineFilename);
QString mdFilename(_testDirectory + "/" + testsOutlineFilename);
QFile mdFile(mdFilename);
if (!mdFile.open(QIODevice::WriteOnly)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename);
@ -764,10 +764,10 @@ void Test::createTestsOutline() {
stream << "Directories with an appended (*) have an automatic test\n\n";
// We need to know our current depth, as this isn't given by QDirIterator
int rootDepth { testDirectory.count('/') };
int rootDepth { _testDirectory.count('/') };
// Each test is shown as the folder name linking to the matching GitHub URL, and the path to the associated test.md file
QDirIterator it(testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
QDirIterator it(_testDirectory.toStdString().c_str(), QDirIterator::Subdirectories);
while (it.hasNext()) {
QString directory = it.next();
@ -821,12 +821,45 @@ void Test::createTestsOutline() {
QMessageBox::information(0, "Success", "Test outline file " + testsOutlineFilename + " has been created");
}
void Test::createTestRailTestSuite() {
QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") {
parent += "/";
}
_testDirectory =
QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", parent, QFileDialog::ShowDirsOnly);
// If user cancelled then restore previous selection and return
if (_testDirectory == "") {
_testDirectory = previousSelection;
return;
}
QString outputDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store generated files in",
parent, QFileDialog::ShowDirsOnly);
// If user cancelled then return
if (outputDirectory == "") {
return;
}
if (_testRailCreateMode == PYTHON) {
_testRailInterface.createTestSuitePython(_testDirectory, outputDirectory, autoTester->getSelectedUser(),
autoTester->getSelectedBranch());
} else {
_testRailInterface.createTestSuiteXML(_testDirectory, outputDirectory, autoTester->getSelectedUser(),
autoTester->getSelectedBranch());
}
}
QStringList Test::createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory) {
imageDirectory = QDir(pathToImageDirectory);
_imageDirectory = QDir(pathToImageDirectory);
QStringList nameFilters;
nameFilters << "*." + imageFormat;
return imageDirectory.entryList(nameFilters, QDir::Files, QDir::Name);
return _imageDirectory.entryList(nameFilters, QDir::Files, QDir::Name);
}
// Snapshots are files in the following format:
@ -889,3 +922,7 @@ QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) {
return result;
}
void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) {
_testRailCreateMode = testRailCreateMode;
}

View file

@ -18,6 +18,7 @@
#include "ImageComparer.h"
#include "ui/MismatchWindow.h"
#include "TestRailInterface.h"
class Step {
public:
@ -33,6 +34,11 @@ public:
StepList stepList;
};
enum TestRailCreateMode {
PYTHON,
XML
};
class Test {
public:
Test();
@ -51,6 +57,8 @@ public:
void createTestsOutline();
void createTestRailTestSuite();
bool compareImageLists(bool isInteractiveMode, QProgressBar* progressBar);
QStringList createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory);
@ -64,11 +72,15 @@ public:
bool createTestResultsFolderPath(const QString& directory);
void zipAndDeleteTestResultsFolder();
bool isAValidDirectory(const QString& pathname);
static bool isAValidDirectory(const QString& pathname);
QString extractPathFromTestsDown(const QString& fullPath);
QString getExpectedImageDestinationDirectory(const QString& filename);
QString getExpectedImagePartialSourceDirectory(const QString& filename);
ExtractedText getTestScriptLines(QString testFileName);
void setTestRailCreateMode(TestRailCreateMode testRailCreateMode);
private:
const QString TEST_FILENAME { "test.js" };
const QString TEST_RESULTS_FOLDER { "TestResults" };
@ -76,14 +88,14 @@ private:
const double THRESHOLD{ 0.96 };
QDir imageDirectory;
QDir _imageDirectory;
MismatchWindow mismatchWindow;
MismatchWindow _mismatchWindow;
ImageComparer imageComparer;
ImageComparer _imageComparer;
QString testResultsFolderPath;
int index { 1 };
QString _testResultsFolderPath;
int _index { 1 };
// Expected images are in the format ExpectedImage_dddd.jpg (d == decimal digit)
const int NUM_DIGITS { 5 };
@ -93,28 +105,30 @@ private:
// The first is the directory containing the test we are working with
// The second is the root directory of all tests
// The third contains the snapshots taken for test runs that need to be evaluated
QString testDirectory;
QString testsRootDirectory;
QString snapshotDirectory;
QString _testDirectory;
QString _testsRootDirectory;
QString _snapshotDirectory;
QStringList expectedImagesFilenames;
QStringList expectedImagesFullFilenames;
QStringList resultImagesFullFilenames;
QStringList _expectedImagesFilenames;
QStringList _expectedImagesFullFilenames;
QStringList _resultImagesFullFilenames;
// Used for accessing GitHub
const QString GIT_HUB_REPOSITORY{ "hifi_tests" };
const QString DATETIME_FORMAT{ "yyyy-MM-dd_hh-mm-ss" };
ExtractedText getTestScriptLines(QString testFileName);
// NOTE: these need to match the appropriate var's in autoTester.js
// var advanceKey = "n";
// var pathSeparator = ".";
const QString ADVANCE_KEY{ "n" };
const QString PATH_SEPARATOR{ "." };
bool exitWhenComplete{ false };
bool _exitWhenComplete{ false };
TestRailInterface _testRailInterface;
TestRailCreateMode _testRailCreateMode { PYTHON };
};
#endif // hifi_test_h

View file

@ -0,0 +1,376 @@
//
// TestRailInterface.cpp
//
// Created by Nissim Hadar on 6 Jul 2018.
// Copyright 2013 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
//
#include "TestRailInterface.h"
#include "Test.h"
#include "ui/TestRailSelectorWindow.h"
#include <QDateTime>
#include <QFile>
#include <QMessageBox>
#include <QTextStream>
TestRailInterface::TestRailInterface() {
_testRailSelectorWindow.setModal(true);
_testRailSelectorWindow.setURL("https://highfidelity.testrail.net/");
_testRailSelectorWindow.setUser("@highfidelity.io");
// 24 is the HighFidelity Interface project id in TestRail
_testRailSelectorWindow.setProject(24);
}
void TestRailInterface::createTestRailDotPyScript(const QString& outputDirectory) {
// Create the testrail.py script
// This is the file linked to from http://docs.gurock.com/testrail-api2/bindings-python
QFile file(outputDirectory + "/testrail.py");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not create \'testrail.py\'");
exit(-1);
}
QTextStream stream(&file);
stream << "#\n";
stream << "# TestRail API binding for Python 3.x (API v2, available since \n";
stream << "# TestRail 3.0)\n";
stream << "#\n";
stream << "# Learn more:\n";
stream << "#\n";
stream << "# http://docs.gurock.com/testrail-api2/start\n";
stream << "# http://docs.gurock.com/testrail-api2/accessing\n";
stream << "#\n";
stream << "# Copyright Gurock Software GmbH. See license.md for details.\n";
stream << "#\n";
stream << "\n";
stream << "import urllib.request, urllib.error\n";
stream << "import json, base64\n";
stream << "\n";
stream << "class APIClient:\n";
stream << "\tdef __init__(self, base_url):\n";
stream << "\t\tself.user = ''\n";
stream << "\t\tself.password = ''\n";
stream << "\t\tif not base_url.endswith('/'):\n";
stream << "\t\t\tbase_url += '/'\n";
stream << "\t\tself.__url = base_url + 'index.php?/api/v2/'\n";
stream << "\n";
stream << "\t#\n";
stream << "\t# Send Get\n";
stream << "\t#\n";
stream << "\t# Issues a GET request (read) against the API and returns the result\n";
stream << "\t# (as Python dict).\n";
stream << "\t#\n";
stream << "\t# Arguments:\n";
stream << "\t#\n";
stream << "\t# uri The API method to call including parameters\n";
stream << "\t# (e.g. get_case/1)\n";
stream << "\t#\n";
stream << "\tdef send_get(self, uri):\n";
stream << "\t\treturn self.__send_request('GET', uri, None)\n";
stream << "\n";
stream << "\t#\n";
stream << "\t# Send POST\n";
stream << "\t#\n";
stream << "\t# Issues a POST request (write) against the API and returns the result\n";
stream << "\t# (as Python dict).\n";
stream << "\t#\n";
stream << "\t# Arguments:\n";
stream << "\t#\n";
stream << "\t# uri The API method to call including parameters\n";
stream << "\t# (e.g. add_case/1)\n";
stream << "\t# data The data to submit as part of the request (as\n";
stream << "\t# Python dict, strings must be UTF-8 encoded)\n";
stream << "\t#\n";
stream << "\tdef send_post(self, uri, data):\n";
stream << "\t\treturn self.__send_request('POST', uri, data)\n";
stream << "\n";
stream << "\tdef __send_request(self, method, uri, data):\n";
stream << "\t\turl = self.__url + uri\n";
stream << "\t\trequest = urllib.request.Request(url)\n";
stream << "\t\tif (method == 'POST'):\n";
stream << "\t\t\trequest.data = bytes(json.dumps(data), 'utf-8')\n";
stream << "\t\tauth = str(\n";
stream << "\t\t\tbase64.b64encode(\n";
stream << "\t\t\t\tbytes('%s:%s' % (self.user, self.password), 'utf-8')\n";
stream << "\t\t\t),\n";
stream << "\t\t\t'ascii'\n";
stream << "\t\t).strip()\n";
stream << "\t\trequest.add_header('Authorization', 'Basic %s' % auth)\n";
stream << "\t\trequest.add_header('Content-Type', 'application/json')\n";
stream << "\n";
stream << "\t\te = None\n";
stream << "\t\ttry:\n";
stream << "\t\t\tresponse = urllib.request.urlopen(request).read()\n";
stream << "\t\texcept urllib.error.HTTPError as ex:\n";
stream << "\t\t\tresponse = ex.read()\n";
stream << "\t\t\te = ex\n";
stream << "\n";
stream << "\t\tif response:\n";
stream << "\t\t\tresult = json.loads(response.decode())\n";
stream << "\t\telse:\n";
stream << "\t\t\tresult = {}\n";
stream << "\n";
stream << "\t\tif e != None:\n";
stream << "\t\t\tif result and 'error' in result:\n";
stream << "\t\t\t\terror = \'\"\' + result[\'error\'] + \'\"\'\n";
stream << "\t\t\telse:\n";
stream << "\t\t\t\terror = \'No additional error message received\'\n";
stream << "\t\t\traise APIError(\'TestRail API returned HTTP %s (%s)\' % \n";
stream << "\t\t\t\t(e.code, error))\n";
stream << "\n";
stream << "\t\treturn result\n";
stream << "\n";
stream << "class APIError(Exception):\n";
stream << "\tpass\n";
file.close();
}
void TestRailInterface::requestDataFromUser() {
_testRailSelectorWindow.exec();
if (_testRailSelectorWindow.getUserCancelled()) {
return;
}
_url = _testRailSelectorWindow.getURL();
_user = _testRailSelectorWindow.getUser();
_password = _testRailSelectorWindow.getPassword();
}
void TestRailInterface::createAddSectionsPythonScript(const QString& outputDirectory) {
QFile file(outputDirectory + "/addSections.py");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not create \'addSections.py\'");
exit(-1);
}
QTextStream stream(&file);
// Code to access TestRail
stream << "from testrail import *\n";
stream << "client = APIClient(\'" << _url.toStdString().c_str() << "\')\n";
stream << "client.user = \'" << _user << "\'\n";
stream << "client.password = \'" << _password << "\'\n\n";
// top-level section
stream << "data = { \'name\': \'"
<< "Test Suite - " << QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm") << "\'}\n";
file.close();
}
void TestRailInterface::createTestSuitePython(const QString& testDirectory,
const QString& outputDirectory,
const QString& user,
const QString& branch) {
createTestRailDotPyScript(outputDirectory);
requestDataFromUser();
createAddSectionsPythonScript(outputDirectory);
}
void TestRailInterface::createTestSuiteXML(const QString& testDirectory,
const QString& outputDirectory,
const QString& user,
const QString& branch) {
QDomProcessingInstruction instruction = _document.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
_document.appendChild(instruction);
// We create a single section, within sections
QDomElement root = _document.createElement("sections");
_document.appendChild(root);
QDomElement topLevelSection = _document.createElement("section");
QDomElement suiteName = _document.createElement("name");
suiteName.appendChild(_document.createTextNode("Test Suite - " + QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm")));
topLevelSection.appendChild(suiteName);
QDomElement secondLevelSections = _document.createElement("sections");
topLevelSection.appendChild(processDirectory(testDirectory, user, branch, secondLevelSections));
topLevelSection.appendChild(secondLevelSections);
root.appendChild(topLevelSection);
// Write to file
const QString testRailsFilename{ outputDirectory + "/TestRailSuite.xml" };
QFile file(testRailsFilename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Could not create XML file");
exit(-1);
}
QTextStream stream(&file);
stream << _document.toString();
file.close();
QMessageBox::information(0, "Success", "TestRail XML file has been created");
}
QDomElement TestRailInterface::processDirectory(const QString& directory, const QString& user, const QString& branch, const QDomElement& element) {
QDomElement result = element;
// Loop over all entries in directory
QDirIterator it(directory.toStdString().c_str());
while (it.hasNext()) {
QString nextDirectory = it.next();
// The object name appears after the last slash (we are assured there is at least 1).
QString objectName = nextDirectory.right(nextDirectory.length() - nextDirectory.lastIndexOf("/") - 1);
// Only process directories
if (Test::isAValidDirectory(nextDirectory)) {
// Ignore the utils and preformance directories
if (nextDirectory.right(QString("utils").length()) == "utils" || nextDirectory.right(QString("performance").length()) == "performance") {
continue;
}
// Create a section and process it
QDomElement sectionElement = _document.createElement("section");
QDomElement sectionElementName = _document.createElement("name");
sectionElementName.appendChild(_document.createTextNode(objectName));
sectionElement.appendChild(sectionElementName);
QDomElement testsElement = _document.createElement("sections");
sectionElement.appendChild(processDirectory(nextDirectory, user, branch, testsElement));
result.appendChild(sectionElement);
} else if (objectName == "test.js" || objectName == "testStory.js") {
QDomElement sectionElement = _document.createElement("section");
QDomElement sectionElementName = _document.createElement("name");
sectionElementName.appendChild(_document.createTextNode("all"));
sectionElement.appendChild(sectionElementName);
sectionElement.appendChild(processTest(nextDirectory, objectName, user, branch, _document.createElement("cases")));
result.appendChild(sectionElement);
}
}
return result;
}
QDomElement TestRailInterface::processTest(const QString& fullDirectory, const QString& test, const QString& user, const QString& branch, const QDomElement& element) {
QDomElement result = element;
QDomElement caseElement = _document.createElement("case");
caseElement.appendChild(_document.createElement("id"));
// The name of the test is derived from the full path.
// The first term is the first word after "tests"
// The last word is the penultimate word
QStringList words = fullDirectory.split('/');
int i = 0;
while (words[i] != "tests") {
++i;
if (i >= words.length() - 1) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder \"tests\" not found in " + fullDirectory);
exit(-1);
}
}
++i;
QString title{ words[i] };
for (++i; i < words.length() - 1; ++i) {
title += " / " + words[i];
}
QDomElement titleElement = _document.createElement("title");
titleElement.appendChild(_document.createTextNode(title));
caseElement.appendChild(titleElement);
QDomElement templateElement = _document.createElement("template");
templateElement.appendChild(_document.createTextNode("Test Case (Steps)"));
caseElement.appendChild(templateElement);
QDomElement typeElement = _document.createElement("type");
typeElement.appendChild(_document.createTextNode("3 - Regression"));
caseElement.appendChild(typeElement);
QDomElement priorityElement = _document.createElement("priority");
priorityElement.appendChild(_document.createTextNode("Medium"));
caseElement.appendChild(priorityElement);
QDomElement estimateElementName = _document.createElement("estimate");
estimateElementName.appendChild(_document.createTextNode("60"));
caseElement.appendChild(estimateElementName);
caseElement.appendChild(_document.createElement("references"));
QDomElement customElement = _document.createElement("custom");
QDomElement tester_countElement = _document.createElement("tester_count");
tester_countElement.appendChild(_document.createTextNode("1"));
customElement.appendChild(tester_countElement);
QDomElement domain_bot_loadElement = _document.createElement("domain_bot_load");
QDomElement domain_bot_loadElementId = _document.createElement("id");
domain_bot_loadElementId.appendChild(_document.createTextNode("1"));
domain_bot_loadElement.appendChild(domain_bot_loadElementId);
QDomElement domain_bot_loadElementValue = _document.createElement("value");
domain_bot_loadElementValue.appendChild(_document.createTextNode(" Without Bots (hifiqa-rc / hifi-qa-stable / hifiqa-master)"));
domain_bot_loadElement.appendChild(domain_bot_loadElementValue);
customElement.appendChild(domain_bot_loadElement);
QDomElement automation_typeElement = _document.createElement("automation_type");
QDomElement automation_typeElementId = _document.createElement("id");
automation_typeElementId.appendChild(_document.createTextNode("0"));
automation_typeElement.appendChild(automation_typeElementId);
QDomElement automation_typeElementValue = _document.createElement("value");
automation_typeElementValue.appendChild(_document.createTextNode("None"));
automation_typeElement.appendChild(automation_typeElementValue);
customElement.appendChild(automation_typeElement);
QDomElement added_to_releaseElement = _document.createElement("added_to_release");
QDomElement added_to_releaseElementId = _document.createElement("id");
added_to_releaseElementId.appendChild(_document.createTextNode("4"));
added_to_releaseElement.appendChild(added_to_releaseElementId);
QDomElement added_to_releaseElementValue = _document.createElement("value");
added_to_releaseElementValue.appendChild(_document.createTextNode(branch));
added_to_releaseElement.appendChild(added_to_releaseElementValue);
customElement.appendChild(added_to_releaseElement);
QDomElement precondsElement = _document.createElement("preconds");
precondsElement.appendChild(_document.createTextNode("Tester is in an empty region of a domain in which they have edit rights\n\n*Note: Press 'n' to advance test script"));
customElement.appendChild(precondsElement);
QString testMDName = QString("https://github.com/") + user + "/hifi_tests/blob/" + branch + "/tests/content/entity/light/point/create/test.md";
QDomElement steps_seperatedElement = _document.createElement("steps_separated");
QDomElement stepElement = _document.createElement("step");
QDomElement stepIndexElement = _document.createElement("index");
stepIndexElement.appendChild(_document.createTextNode("1"));
stepElement.appendChild(stepIndexElement);
QDomElement stepContentElement = _document.createElement("content");
stepContentElement.appendChild(_document.createTextNode(QString("Execute instructions in [THIS TEST](") + testMDName + ")"));
stepElement.appendChild(stepContentElement);
QDomElement stepExpectedElement = _document.createElement("expected");
stepExpectedElement.appendChild(_document.createTextNode("Refer to the expected result in the linked description."));
stepElement.appendChild(stepExpectedElement);
steps_seperatedElement.appendChild(stepElement);
customElement.appendChild(steps_seperatedElement);
QDomElement notesElement = _document.createElement("notes");
notesElement.appendChild(_document.createTextNode(testMDName));
customElement.appendChild(notesElement);
caseElement.appendChild(customElement);
result.appendChild(caseElement);
return result;
}

View file

@ -0,0 +1,54 @@
//
// TestRailInterface.h
//
// Created by Nissim Hadar on 6 Jul 2018.
// Copyright 2013 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
//
#ifndef hifi_test_testrail_interface_h
#define hifi_test_testrail_interface_h
#include "ui/TestRailSelectorWindow.h"
#include <QDirIterator>
#include <QtXml/QDomDocument>
#include <QString>
class TestRailInterface {
public:
TestRailInterface();
void createTestSuiteXML(const QString& testDirectory,
const QString& outputDirectory,
const QString& user,
const QString& branch);
void createTestSuitePython(const QString& testDirectory,
const QString& outputDirectory,
const QString& user,
const QString& branch);
QDomElement processDirectory(const QString& directory,
const QString& user,
const QString& branch,
const QDomElement& element);
QDomElement processTest(const QString& fullDirectory, const QString& test, const QString& user, const QString& branch, const QDomElement& element);
void createTestRailDotPyScript(const QString& outputDirectory);
void requestDataFromUser();
void createAddSectionsPythonScript(const QString& outputDirectory);
private:
QDomDocument _document;
TestRailSelectorWindow _testRailSelectorWindow;
QString _url;
QString _user;
QString _password;
};
#endif

View file

@ -16,56 +16,60 @@
#endif
AutoTester::AutoTester(QWidget *parent) : QMainWindow(parent) {
ui.setupUi(this);
ui.checkBoxInteractiveMode->setChecked(true);
ui.progressBar->setVisible(false);
_ui.setupUi(this);
_ui.checkBoxInteractiveMode->setChecked(true);
_ui.progressBar->setVisible(false);
signalMapper = new QSignalMapper();
_signalMapper = new QSignalMapper();
connect(ui.actionClose, &QAction::triggered, this, &AutoTester::on_closeButton_clicked);
connect(ui.actionAbout, &QAction::triggered, this, &AutoTester::about);
connect(_ui.actionClose, &QAction::triggered, this, &AutoTester::on_closeButton_clicked);
connect(_ui.actionAbout, &QAction::triggered, this, &AutoTester::about);
#ifndef Q_OS_WIN
ui.hideTaskbarButton->setVisible(false);
ui.showTaskbarButton->setVisible(false);
_ui.hideTaskbarButton->setVisible(false);
_ui.showTaskbarButton->setVisible(false);
#endif
}
void AutoTester::setup() {
test = new Test();
_test = new Test();
}
void AutoTester::runFromCommandLine(const QString& testFolder, const QString& branch, const QString& user) {
isRunningFromCommandline = true;
test->startTestsEvaluation(testFolder, branch, user);
_isRunningFromCommandline = true;
_test->startTestsEvaluation(testFolder, branch, user);
}
void AutoTester::on_evaluateTestsButton_clicked() {
test->startTestsEvaluation();
_test->startTestsEvaluation();
}
void AutoTester::on_createRecursiveScriptButton_clicked() {
test->createRecursiveScript();
_test->createRecursiveScript();
}
void AutoTester::on_createAllRecursiveScriptsButton_clicked() {
test->createAllRecursiveScripts();
_test->createAllRecursiveScripts();
}
void AutoTester::on_createTestsButton_clicked() {
test->createTests();
_test->createTests();
}
void AutoTester::on_createMDFileButton_clicked() {
test->createMDFile();
_test->createMDFile();
}
void AutoTester::on_createAllMDFilesButton_clicked() {
test->createAllMDFiles();
_test->createAllMDFiles();
}
void AutoTester::on_createTestsOutlineButton_clicked() {
test->createTestsOutline();
_test->createTestsOutline();
}
void AutoTester::on_createTestRailTestSuiteButton_clicked() {
_test->createTestRailTestSuite();
}
// To toggle between show and hide
@ -96,11 +100,19 @@ void AutoTester::on_closeButton_clicked() {
exit(0);
}
void AutoTester::downloadImage(const QUrl& url) {
downloaders.emplace_back(new Downloader(url, this));
connect(downloaders[_index], SIGNAL (downloaded()), signalMapper, SLOT (map()));
void AutoTester::on_createPythonScriptRadioButton_clicked() {
_test->setTestRailCreateMode(PYTHON);
}
signalMapper->setMapping(downloaders[_index], _index);
void AutoTester::on_createXMLScriptRadioButton_clicked() {
_test->setTestRailCreateMode(XML);
}
void AutoTester::downloadImage(const QUrl& url) {
_downloaders.emplace_back(new Downloader(url, this));
connect(_downloaders[_index], SIGNAL (downloaded()), _signalMapper, SLOT (map()));
_signalMapper->setMapping(_downloaders[_index], _index);
++_index;
}
@ -113,39 +125,39 @@ void AutoTester::downloadImages(const QStringList& URLs, const QString& director
_numberOfImagesDownloaded = 0;
_index = 0;
ui.progressBar->setMinimum(0);
ui.progressBar->setMaximum(_numberOfImagesToDownload - 1);
ui.progressBar->setValue(0);
ui.progressBar->setVisible(true);
_ui.progressBar->setMinimum(0);
_ui.progressBar->setMaximum(_numberOfImagesToDownload - 1);
_ui.progressBar->setValue(0);
_ui.progressBar->setVisible(true);
downloaders.clear();
_downloaders.clear();
for (int i = 0; i < _numberOfImagesToDownload; ++i) {
QUrl imageURL(URLs[i]);
downloadImage(imageURL);
}
connect(signalMapper, SIGNAL (mapped(int)), this, SLOT (saveImage(int)));
connect(_signalMapper, SIGNAL (mapped(int)), this, SLOT (saveImage(int)));
}
void AutoTester::saveImage(int index) {
try {
QFile file(_directoryName + "/" + _filenames[index]);
file.open(QIODevice::WriteOnly);
file.write(downloaders[index]->downloadedData());
file.write(_downloaders[index]->downloadedData());
file.close();
} catch (...) {
QMessageBox::information(0, "Test Aborted", "Failed to save image: " + _filenames[index]);
ui.progressBar->setVisible(false);
_ui.progressBar->setVisible(false);
return;
}
++_numberOfImagesDownloaded;
if (_numberOfImagesDownloaded == _numberOfImagesToDownload) {
disconnect(signalMapper, SIGNAL (mapped(int)), this, SLOT (saveImage(int)));
test->finishTestsEvaluation(isRunningFromCommandline, ui.checkBoxInteractiveMode->isChecked(), ui.progressBar);
disconnect(_signalMapper, SIGNAL (mapped(int)), this, SLOT (saveImage(int)));
_test->finishTestsEvaluation(_isRunningFromCommandline, _ui.checkBoxInteractiveMode->isChecked(), _ui.progressBar);
} else {
ui.progressBar->setValue(_numberOfImagesDownloaded);
_ui.progressBar->setValue(_numberOfImagesDownloaded);
}
}
@ -154,18 +166,18 @@ void AutoTester::about() {
}
void AutoTester::setUserText(const QString& user) {
ui.userTextEdit->setText(user);
_ui.userTextEdit->setText(user);
}
QString AutoTester::getSelectedUser()
{
return ui.userTextEdit->toPlainText();
return _ui.userTextEdit->toPlainText();
}
void AutoTester::setBranchText(const QString& branch) {
ui.branchTextEdit->setText(branch);
_ui.branchTextEdit->setText(branch);
}
QString AutoTester::getSelectedBranch() {
return ui.branchTextEdit->toPlainText();
return _ui.branchTextEdit->toPlainText();
}

View file

@ -45,10 +45,14 @@ private slots:
void on_createMDFileButton_clicked();
void on_createAllMDFilesButton_clicked();
void on_createTestsOutlineButton_clicked();
void on_createTestRailTestSuiteButton_clicked();
void on_hideTaskbarButton_clicked();
void on_showTaskbarButton_clicked();
void on_createPythonScriptRadioButton_clicked();
void on_createXMLScriptRadioButton_clicked();
void on_closeButton_clicked();
void saveImage(int index);
@ -56,23 +60,23 @@ private slots:
void about();
private:
Ui::AutoTesterClass ui;
Test* test;
Ui::AutoTesterClass _ui;
Test* _test;
std::vector<Downloader*> downloaders;
std::vector<Downloader*> _downloaders;
// local storage for parameters - folder to store downloaded files in, and a list of their names
QString _directoryName;
QStringList _filenames;
// Used to enable passing a parameter to slots
QSignalMapper* signalMapper;
QSignalMapper* _signalMapper;
int _numberOfImagesToDownload { 0 };
int _numberOfImagesDownloaded { 0 };
int _index { 0 };
bool isRunningFromCommandline { false };
bool _isRunningFromCommandline { false };
};
#endif // hifi_AutoTester_h

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>612</width>
<height>537</height>
<width>645</width>
<height>570</height>
</rect>
</property>
<property name="windowTitle">
@ -18,7 +18,7 @@
<property name="geometry">
<rect>
<x>380</x>
<y>430</y>
<y>450</y>
<width>101</width>
<height>40</height>
</rect>
@ -44,7 +44,7 @@
<property name="geometry">
<rect>
<x>430</x>
<y>270</y>
<y>320</y>
<width>101</width>
<height>40</height>
</rect>
@ -57,7 +57,7 @@
<property name="geometry">
<rect>
<x>330</x>
<y>110</y>
<y>170</y>
<width>220</width>
<height>40</height>
</rect>
@ -70,7 +70,7 @@
<property name="geometry">
<rect>
<x>320</x>
<y>280</y>
<y>330</y>
<width>131</width>
<height>20</height>
</rect>
@ -86,7 +86,7 @@
<property name="geometry">
<rect>
<x>320</x>
<y>330</y>
<y>380</y>
<width>255</width>
<height>23</height>
</rect>
@ -99,7 +99,7 @@
<property name="geometry">
<rect>
<x>330</x>
<y>170</y>
<y>230</y>
<width>220</width>
<height>40</height>
</rect>
@ -229,13 +229,55 @@
</rect>
</property>
</widget>
<widget class="QPushButton" name="createTestRailTestSuiteButton">
<property name="geometry">
<rect>
<x>409</x>
<y>100</y>
<width>141</width>
<height>40</height>
</rect>
</property>
<property name="text">
<string>Create TestRail Test Suite</string>
</property>
</widget>
<widget class="QRadioButton" name="createPythonScriptRadioButton">
<property name="geometry">
<rect>
<x>310</x>
<y>100</y>
<width>95</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>Python</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<widget class="QRadioButton" name="createXMLScriptRadioButton">
<property name="geometry">
<rect>
<x>310</x>
<y>120</y>
<width>95</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>XML</string>
</property>
</widget>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>612</width>
<width>645</width>
<height>21</height>
</rect>
</property>

View file

@ -66,14 +66,14 @@ void MismatchWindow::setTestFailure(TestFailure testFailure) {
QPixmap expectedPixmap = QPixmap(testFailure._pathname + testFailure._expectedImageFilename);
QPixmap actualPixmap = QPixmap(testFailure._pathname + testFailure._actualImageFilename);
diffPixmap = computeDiffPixmap(
_diffPixmap = computeDiffPixmap(
QImage(testFailure._pathname + testFailure._expectedImageFilename),
QImage(testFailure._pathname + testFailure._actualImageFilename)
);
expectedImage->setPixmap(expectedPixmap);
resultImage->setPixmap(actualPixmap);
diffImage->setPixmap(diffPixmap);
diffImage->setPixmap(_diffPixmap);
}
void MismatchWindow::on_passTestButton_clicked() {
@ -92,5 +92,5 @@ void MismatchWindow::on_abortTestsButton_clicked() {
}
QPixmap MismatchWindow::getComparisonImage() {
return diffPixmap;
return _diffPixmap;
}

View file

@ -36,7 +36,7 @@ private slots:
private:
UserResponse _userResponse{ USER_RESPONSE_INVALID };
QPixmap diffPixmap;
QPixmap _diffPixmap;
};

View file

@ -0,0 +1,62 @@
//
// TestRailSelectorWindow.cpp
//
// Created by Nissim Hadar on 26 Jul 2017.
// Copyright 2013 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
//
#include "TestRailSelectorWindow.h"
#include <QtCore/QFileInfo>
#include <cmath>
TestRailSelectorWindow::TestRailSelectorWindow(QWidget *parent) {
setupUi(this);
projectLineEdit->setValidator(new QIntValidator(1, 999, this));
}
void TestRailSelectorWindow::on_OKButton_clicked() {
userCancelled = false;
close();
}
void TestRailSelectorWindow::on_cancelButton_clicked() {
userCancelled = true;
close();
}
bool TestRailSelectorWindow::getUserCancelled() {
return userCancelled;
}
void TestRailSelectorWindow::setURL(const QString& user) {
URLTextEdit->setText(user);
}
QString TestRailSelectorWindow::getURL() {
return URLTextEdit->toPlainText();
}
void TestRailSelectorWindow::setUser(const QString& user) {
userTextEdit->setText(user);
}
QString TestRailSelectorWindow::getUser() {
return userTextEdit->toPlainText();
}
QString TestRailSelectorWindow::getPassword() {
return passwordLineEdit->text();
}
void TestRailSelectorWindow::setProject(const int project) {
projectLineEdit->setText(QString::number(project));
}
int TestRailSelectorWindow::getProject() {
return projectLineEdit->text().toInt();
}

View file

@ -0,0 +1,41 @@
//
// TestRailSelectorWindow.h
//
// Created by Nissim Hadar on 26 Jul 2017.
// Copyright 2013 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
//
#ifndef hifi_TestRailSelectorWindow_h
#define hifi_TestRailSelectorWindow_h
#include "ui_TestRailSelectorWindow.h"
class TestRailSelectorWindow : public QDialog, public Ui::TestRailSelectorWindow {
Q_OBJECT
public:
TestRailSelectorWindow(QWidget* parent = Q_NULLPTR);
bool getUserCancelled();
void setURL(const QString& user);
QString getURL();
void setUser(const QString& user);
QString getUser();
QString getPassword();
void setProject(const int project);
int getProject();
bool userCancelled{ false };
private slots:
void on_OKButton_clicked();
void on_cancelButton_clicked();
};
#endif

View file

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TestRailSelectorWindow</class>
<widget class="QDialog" name="TestRailSelectorWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>489</width>
<height>312</height>
</rect>
</property>
<property name="windowTitle">
<string>MismatchWindow</string>
</property>
<widget class="QLabel" name="errorLabel">
<property name="geometry">
<rect>
<x>30</x>
<y>850</y>
<width>500</width>
<height>28</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>similarity</string>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>70</x>
<y>115</y>
<width>121</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>TestRail Password</string>
</property>
</widget>
<widget class="QLabel" name="label_2">
<property name="geometry">
<rect>
<x>70</x>
<y>25</y>
<width>121</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>TestRail URL</string>
</property>
</widget>
<widget class="QTextEdit" name="URLTextEdit">
<property name="geometry">
<rect>
<x>200</x>
<y>25</y>
<width>231</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="OKButton">
<property name="geometry">
<rect>
<x>120</x>
<y>240</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>OK</string>
</property>
</widget>
<widget class="QPushButton" name="cancelButton">
<property name="geometry">
<rect>
<x>280</x>
<y>240</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
<widget class="QLineEdit" name="passwordLineEdit">
<property name="geometry">
<rect>
<x>200</x>
<y>115</y>
<width>231</width>
<height>24</height>
</rect>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
<widget class="QTextEdit" name="userTextEdit">
<property name="geometry">
<rect>
<x>200</x>
<y>70</y>
<width>231</width>
<height>24</height>
</rect>
</property>
</widget>
<widget class="QLabel" name="label_3">
<property name="geometry">
<rect>
<x>70</x>
<y>70</y>
<width>121</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>TestRail User</string>
</property>
</widget>
<widget class="QLineEdit" name="projectLineEdit">
<property name="geometry">
<rect>
<x>200</x>
<y>160</y>
<width>231</width>
<height>24</height>
</rect>
</property>
<property name="echoMode">
<enum>QLineEdit::Normal</enum>
</property>
</widget>
<widget class="QLabel" name="label_4">
<property name="geometry">
<rect>
<x>70</x>
<y>160</y>
<width>121</width>
<height>20</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>TestRail Project</string>
</property>
</widget>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>