Merge remote-tracking branch 'upstream/master' into 21146

This commit is contained in:
Triplelexx 2017-02-02 01:16:51 +00:00
commit 05e3acf1a3
29 changed files with 927 additions and 191 deletions

View file

@ -125,6 +125,10 @@ tr.new-row {
background-color: #dff0d8;
}
tr.invalid-input {
background-color: #f2dede;
}
.graphable-stat {
text-align: center;
color: #5286BC;

View file

@ -38,14 +38,15 @@ var Settings = {
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
PLACES_TABLE_ID: 'places-table',
FORM_ID: 'settings-form'
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input'
};
var viewHelpers = {
getFormGroup: function(keypath, setting, values, isAdvanced) {
form_group = "<div class='form-group " +
(isAdvanced ? Settings.ADVANCED_CLASS : "") + " " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
"data-keypath='" + keypath + "'>";
setting_value = _(values).valueForKeyPath(keypath);
@ -215,8 +216,8 @@ $(document).ready(function(){
sibling = sibling.next();
}
if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click();
// for tables with categories we add the entry and setup the new row on enter
if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) {
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
// set focus to the first input in the new row
@ -891,48 +892,145 @@ function reloadSettings(callback) {
});
}
function validateInputs() {
// check if any new values are bad
var tables = $('table');
var inputsValid = true;
var tables = $('table');
// clear any current invalid rows
$('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS);
function markParentRowInvalid(rowChild) {
$(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS);
}
_.each(tables, function(table) {
// validate keys specificially for spaces and equality to an existing key
var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key');
var keyWithSpaces = false;
var empty = false;
var duplicateKey = false;
_.each(newKeys, function(keyCell) {
var keyVal = $(keyCell).children('input').val();
if (keyVal.indexOf(' ') !== -1) {
keyWithSpaces = true;
markParentRowInvalid(keyCell);
return;
}
// make sure the key isn't empty
if (keyVal.length === 0) {
empty = true
markParentRowInvalid(input);
return;
}
// make sure we don't have duplicate keys in the table
var otherKeys = $(table).find('td.key').not(keyCell);
_.each(otherKeys, function(otherKeyCell) {
var keyInput = $(otherKeyCell).children('input');
if (keyInput.length) {
if ($(keyInput).val() == keyVal) {
duplicateKey = true;
}
} else if ($(otherKeyCell).html() == keyVal) {
duplicateKey = true;
}
if (duplicateKey) {
markParentRowInvalid(keyCell);
return;
}
});
});
if (keyWithSpaces) {
showErrorMessage("Error", "Key contains spaces");
inputsValid = false;
return
}
if (empty) {
showErrorMessage("Error", "Empty field(s)");
inputsValid = false;
return
}
if (duplicateKey) {
showErrorMessage("Error", "Two keys cannot be identical");
inputsValid = false;
return;
}
});
return inputsValid;
}
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
function saveSettings() {
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
if (validateInputs()) {
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
// check if we've set the basic http password - if so convert it to base64
var canPost = true;
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
var verify_password = formJSON["security"]["verify_http_password"];
if (password && password.length > 0) {
if (password != verify_password) {
bootbox.alert({"message": "Passwords must match!", "title":"Password Error"});
canPost = false;
} else {
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
// check if we've set the basic http password - if so convert it to base64
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
if (password && password.length > 0) {
formJSON["security"]["http_password"] = sha256_digest(password);
delete formJSON["security"]["verify_http_password"];
}
}
}
console.log("----- SAVING ------");
console.log(formJSON);
// verify that the password and confirmation match before saving
var canPost = true;
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
var verify_password = formJSON["security"]["verify_http_password"];
// remove focus from the button
$(this).blur();
if (password && password.length > 0) {
if (password != verify_password) {
bootbox.alert({"message": "Passwords must match!", "title":"Password Error"});
canPost = false;
} else {
formJSON["security"]["http_password"] = sha256_digest(password);
delete formJSON["security"]["verify_http_password"];
}
}
}
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
if (canPost) {
postSettings(formJSON);
console.log("----- SAVING ------");
console.log(formJSON);
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
// remove focus from the button
$(this).blur();
if (canPost) {
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
postSettings(formJSON);
}
}
}
@ -1110,8 +1208,9 @@ function makeTable(setting, keypath, setting_value) {
if (setting.can_add_new_categories) {
html += makeTableCategoryInput(setting, numVisibleColumns);
}
if (setting.can_add_new_rows || setting.can_add_new_categories) {
html += makeTableInputs(setting, {}, "");
html += makeTableHiddenInputs(setting, {}, "");
}
}
html += "</table>"
@ -1137,7 +1236,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns,
return html;
}
function makeTableInputs(setting, initialValues, categoryValue) {
function makeTableHiddenInputs(setting, initialValues, categoryValue) {
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
@ -1148,7 +1247,7 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (setting.key) {
html += "<td class='key' name='" + setting.key.name + "'>\
<input type='text' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
<input type='text' style='display: none;' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
</td>"
}
@ -1157,14 +1256,14 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (col.type === "checkbox") {
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
"<input type='checkbox' class='form-control table-checkbox' " +
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
"</td>";
} else {
html +=
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
"name='" + col.name + "'>" +
"<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
"<input type='text' style='display: none;' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
(col.readonly ? " readonly" : "") + ">" +
"</td>";
@ -1244,49 +1343,17 @@ function addTableRow(row) {
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
var input_clone = row.clone();
if (!isArray) {
// Check key spaces
var key = row.children(".key").children("input").val()
if (key.indexOf(' ') !== -1) {
showErrorMessage("Error", "Key contains spaces")
return
}
// Check keys with the same name
var equals = false;
_.each(columns.children(".key"), function(element) {
if ($(element).text() === key) {
equals = true
return
}
})
if (equals) {
showErrorMessage("Error", "Two keys cannot be identical")
return
}
// show the key input
var keyInput = row.children(".key").children("input");
}
// Check empty fields
var empty = false;
_.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) {
if ($(element).val().length === 0) {
empty = true
return
}
})
if (empty) {
showErrorMessage("Error", "Empty field(s)")
return
}
var input_clone = row.clone()
// Change input row to data row
var table = row.parents("table")
var setting_name = table.attr("name")
var full_name = setting_name + "." + key
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS)
row.removeClass("inputs")
var table = row.parents("table");
var setting_name = table.attr("name");
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
_.each(row.children(), function(element) {
if ($(element).hasClass("numbered")) {
@ -1308,56 +1375,43 @@ function addTableRow(row) {
anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES)
} else if ($(element).hasClass("key")) {
var input = $(element).children("input")
$(element).html(input.val())
input.remove()
input.show();
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
// Hide inputs
var input = $(element).find("input")
var isCheckbox = false;
var isTime = false;
if (input.hasClass("table-checkbox")) {
input = $(input).parent();
isCheckbox = true;
} else if (input.hasClass("table-time")) {
input = $(input).parent();
isTime = true;
}
// show inputs
var input = $(element).find("input");
input.show();
var val = input.val();
if (isCheckbox) {
// don't hide the checkbox
val = $(input).find("input").is(':checked');
} else if (isTime) {
// don't hide the time
} else {
input.attr("type", "hidden")
}
var isCheckbox = input.hasClass("table-checkbox");
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
var key = $(element).attr('name')
var key = $(element).attr('name');
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
if (isCheckbox) {
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
}
} else {
input.attr("name", full_name + "." + $(element).attr("name"))
// because the name of the setting in question requires the key
// setup a hook to change the HTML name of the element whenever the key changes
var colName = $(element).attr("name");
keyInput.on('change', function(){
input.attr("name", setting_name + "." + $(this).val() + "." + colName);
});
}
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
input.attr("data-changed", "true");
$(element).append(val);
}
} else {
console.log("Unknown table element")
console.log("Unknown table element");
}
});
@ -1387,7 +1441,12 @@ function deleteTableRow($row) {
$row.empty();
if (!isArray) {
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
if ($row.attr('name')) {
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
} else {
// for rows that didn't have a key, simply remove the row
$row.remove();
}
} else {
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
// This is the last row of the category, so delete the header

View file

@ -33,6 +33,7 @@ Item {
propagateComposedEvents: true
acceptedButtons: "AllButtons"
onClicked: {
menu.visible = false;
menu.done();
mouse.accepted = false;
}

View file

@ -32,6 +32,7 @@ Item {
MouseArea {
anchors.fill: parent
onClicked: {
menu.visible = false;
root.triggered();
menu.done();
}

View file

@ -34,6 +34,7 @@ Item {
property bool isMyCard: false
property bool selected: false
property bool isAdmin: false
property bool currentlyEditingDisplayName: false
/* User image commented out for now - will probably be re-introduced later.
Column {
@ -104,6 +105,7 @@ Item {
focus = false
myDisplayName.border.width = 0
color = hifi.colors.darkGray
currentlyEditingDisplayName = false
}
}
MouseArea {
@ -115,10 +117,12 @@ Item {
myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll();
myDisplayNameText.focus = true
myDisplayNameText.color = "black"
currentlyEditingDisplayName = true
}
onDoubleClicked: {
myDisplayNameText.selectAll();
myDisplayNameText.focus = true;
currentlyEditingDisplayName = true
}
onEntered: myDisplayName.color = hifi.colors.lightGrayText
onExited: myDisplayName.color = hifi.colors.textFieldLightBackground

View file

@ -51,6 +51,7 @@ Rectangle {
// This is the container for the PAL
Rectangle {
property bool punctuationMode: false
id: palContainer
// Size
width: pal.width - 50
@ -421,6 +422,16 @@ Rectangle {
onExited: adminHelpText.color = hifi.colors.redHighlight
}
}
HifiControls.Keyboard {
id: keyboard
raised: myCard.currentlyEditingDisplayName && HMD.active
numeric: parent.punctuationMode
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
}
// Timer used when selecting table rows that aren't yet present in the model
// (i.e. when selecting avatars using edit.js or sphere overlays)

View file

@ -75,6 +75,14 @@ Item {
source: buttonOutline
}
function urlHelper(src) {
if (src.match(/\bhttp/)) {
return src;
} else {
return "../../../" + src;
}
}
Image {
id: icon
width: 50
@ -84,7 +92,7 @@ Item {
anchors.bottomMargin: 5
anchors.horizontalCenter: parent.horizontalCenter
fillMode: Image.Stretch
source: "../../../" + tabletButton.icon
source: tabletButton.urlHelper(tabletButton.icon)
}
ColorOverlay {
@ -185,7 +193,7 @@ Item {
PropertyChanges {
target: icon
source: "../../../" + tabletButton.activeIcon
source: tabletButton.urlHelper(tabletButton.activeIcon)
}
},
State {

View file

@ -1202,8 +1202,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
setKeyboardFocusEntity(entityItemID);
auto entity = getEntities()->getTree()->findEntityByID(entityItemID);
if (entity && entity->wantsKeyboardFocus()) {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
setKeyboardFocusEntity(entityItemID);
}
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
@ -4165,6 +4168,8 @@ void Application::setKeyboardFocusOverlay(unsigned int overlayID) {
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
const float OVERLAY_DEPTH = 0.0105f;
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
} else if (_keyboardFocusHighlight) {
_keyboardFocusHighlight->setVisible(false);
}
}
}

View file

@ -18,7 +18,7 @@
QString const ModelOverlay::TYPE = "model";
ModelOverlay::ModelOverlay()
: _model(std::make_shared<Model>(std::make_shared<Rig>())),
: _model(std::make_shared<Model>(std::make_shared<Rig>(), nullptr, this)),
_modelTextures(QVariantMap())
{
_model->init();
@ -27,7 +27,7 @@ ModelOverlay::ModelOverlay()
ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) :
Volume3DOverlay(modelOverlay),
_model(std::make_shared<Model>(std::make_shared<Rig>())),
_model(std::make_shared<Model>(std::make_shared<Rig>(), nullptr, this)),
_modelTextures(QVariantMap()),
_url(modelOverlay->_url),
_updateModel(false)

View file

@ -540,7 +540,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha
std::static_pointer_cast<EntityTree>(_tree)->processEraseMessage(message, sourceNode);
}
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority) {
ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority, SpatiallyNestable* spatiallyNestableOverride) {
ModelPointer model = nullptr;
// Only create and delete models on the thread that owns the EntityTreeRenderer
@ -552,7 +552,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading
return model;
}
model = std::make_shared<Model>(std::make_shared<Rig>());
model = std::make_shared<Model>(std::make_shared<Rig>(), nullptr, spatiallyNestableOverride);
model->setLoadingPriority(loadingPriority);
model->init();
model->setURL(QUrl(url));

View file

@ -77,7 +77,7 @@ public:
void reloadEntityScripts();
/// if a renderable entity item needs a model, we will allocate it for them
Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f);
Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f, SpatiallyNestable* spatiallyNestableOverride = nullptr);
/// if a renderable entity item needs to update the URL of a model, we will handle that for the entity
Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl);

View file

@ -504,8 +504,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointer<EntityTreeRender
if (!getModelURL().isEmpty()) {
// If we don't have a model, allocate one *immediately*
if (!_model) {
_model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this));
_model->setSpatiallyNestableOverride(shared_from_this());
_model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this), this);
_needsInitialSimulation = true;
// If we need to change URLs, update it *after rendering* (to avoid access violations)
} else if (QUrl(getModelURL()) != _model->getURL()) {

View file

@ -651,6 +651,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
// NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data
// even when we would otherwise ignore the rest of the packet.
bool filterRejection = false;
if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) {
QByteArray simOwnerData;
@ -663,6 +664,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
if (wantTerseEditLogging() && _simulationOwner != newSimOwner) {
qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner;
}
// This is used in the custom physics setters, below. When an entity-server filter alters
// or rejects a set of properties, it clears this. In such cases, we don't want those custom
// setters to ignore what the server says.
filterRejection = newSimOwner.getID().isNull();
if (weOwnSimulation) {
if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) {
// entity-server is trying to clear our ownership (probably at our own request)
@ -688,6 +693,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
somethingChanged = true;
_simulationOwner.clearCurrentOwner();
}
} else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) {
// entity-server tells us that we have simulation ownership while we never requested this for this EntityItem,
// this could happen when the user reloads the cache and entity tree.
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
somethingChanged = true;
_simulationOwner.clearCurrentOwner();
weOwnSimulation = false;
} else if (_simulationOwner.set(newSimOwner)) {
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
somethingChanged = true;
@ -708,55 +720,45 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
// Note: duplicate packets are expected and not wrong. They may be sent for any number of
// reasons and the contract is that the client handles them in an idempotent manner.
auto lastEdited = lastEditedFromBufferAdjusted;
auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp;
bool valueChanged = value != _lastUpdatedPositionValue;
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
if (shouldUpdate) {
bool otherOverwrites = overwriteLocalData && !weOwnSimulation;
auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
bool simulationChanged = lastEdited > updatedTimestamp;
return otherOverwrites && simulationChanged && (valueChanged || filterRejection);
};
auto customUpdatePositionFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
if (shouldUpdate(_lastUpdatedPositionTimestamp, value != _lastUpdatedPositionValue)) {
updatePositionFromNetwork(value);
_lastUpdatedPositionTimestamp = lastEdited;
_lastUpdatedPositionValue = value;
}
};
auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::quat value){
bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp;
bool valueChanged = value != _lastUpdatedRotationValue;
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
if (shouldUpdate) {
auto customUpdateRotationFromNetwork = [this, shouldUpdate, lastEdited](glm::quat value){
if (shouldUpdate(_lastUpdatedRotationTimestamp, value != _lastUpdatedRotationValue)) {
updateRotationFromNetwork(value);
_lastUpdatedRotationTimestamp = lastEdited;
_lastUpdatedRotationValue = value;
}
};
auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp;
bool valueChanged = value != _lastUpdatedVelocityValue;
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
if (shouldUpdate) {
auto customUpdateVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
if (shouldUpdate(_lastUpdatedVelocityTimestamp, value != _lastUpdatedVelocityValue)) {
updateVelocityFromNetwork(value);
_lastUpdatedVelocityTimestamp = lastEdited;
_lastUpdatedVelocityValue = value;
}
};
auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp;
bool valueChanged = value != _lastUpdatedAngularVelocityValue;
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
if (shouldUpdate) {
auto customUpdateAngularVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){
if (shouldUpdate(_lastUpdatedAngularVelocityTimestamp, value != _lastUpdatedAngularVelocityValue)) {
updateAngularVelocityFromNetwork(value);
_lastUpdatedAngularVelocityTimestamp = lastEdited;
_lastUpdatedAngularVelocityValue = value;
}
};
auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){
bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp;
bool valueChanged = value != _lastUpdatedAccelerationValue;
bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged;
if (shouldUpdate) {
auto customSetAcceleration = [this, shouldUpdate, lastEdited](glm::vec3 value){
if (shouldUpdate(_lastUpdatedAccelerationTimestamp, value != _lastUpdatedAccelerationValue)) {
setAcceleration(value);
_lastUpdatedAccelerationTimestamp = lastEdited;
_lastUpdatedAccelerationValue = value;
@ -1278,7 +1280,7 @@ void EntityItem::grabSimulationOwnership() {
auto nodeList = DependencyManager::get<NodeList>();
if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) {
// we already own it
_simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY);
_simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY);
} else {
// we don't own it yet
_simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow());
@ -1889,6 +1891,10 @@ void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& tim
_simulationOwner.setPendingPriority(priority, timestamp);
}
void EntityItem::rememberHasSimulationOwnershipBid() const {
_hasBidOnSimulation = true;
}
QString EntityItem::actionsToDebugString() {
QString result;
QVector<QByteArray> serializedActions;

View file

@ -321,6 +321,7 @@ public:
void updateSimulationOwner(const SimulationOwner& owner);
void clearSimulationOwnership();
void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp);
void rememberHasSimulationOwnershipBid() const;
const QString& getMarketplaceID() const { return _marketplaceID; }
void setMarketplaceID(const QString& value) { _marketplaceID = value; }
@ -497,16 +498,16 @@ protected:
mutable AABox _cachedAABox;
mutable AACube _maxAACube;
mutable AACube _minAACube;
mutable bool _recalcAABox = true;
mutable bool _recalcMinAACube = true;
mutable bool _recalcMaxAACube = true;
mutable bool _recalcAABox { true };
mutable bool _recalcMinAACube { true };
mutable bool _recalcMaxAACube { true };
float _localRenderAlpha;
float _density = ENTITY_ITEM_DEFAULT_DENSITY; // kg/m^3
float _density { ENTITY_ITEM_DEFAULT_DENSITY }; // kg/m^3
// NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class
// rather than in all of the derived classes. If we ever collapse these classes to one we could do it a
// different way.
float _volumeMultiplier = 1.0f;
float _volumeMultiplier { 1.0f };
glm::vec3 _gravity;
glm::vec3 _acceleration;
float _damping;
@ -516,7 +517,7 @@ protected:
QString _script; /// the value of the script property
QString _loadedScript; /// the value of _script when the last preload signal was sent
quint64 _scriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload
quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload
QString _serverScripts;
/// keep track of time when _serverScripts property was last changed
@ -524,7 +525,7 @@ protected:
/// the value of _scriptTimestamp when the last preload signal was sent
// NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it
quint64 _loadedScriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 };
quint64 _loadedScriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 };
QString _collisionSoundURL;
SharedSoundPointer _collisionSound;
@ -562,8 +563,8 @@ protected:
uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation
// these backpointers are only ever set/cleared by friends:
EntityTreeElementPointer _element = nullptr; // set by EntityTreeElement
void* _physicsInfo = nullptr; // set by EntitySimulation
EntityTreeElementPointer _element { nullptr }; // set by EntityTreeElement
void* _physicsInfo { nullptr }; // set by EntitySimulation
bool _simulated; // set by EntitySimulation
bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action);
@ -580,12 +581,15 @@ protected:
// are used to keep track of and work around this situation.
void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr);
mutable QSet<QUuid> _actionsToRemove;
mutable bool _actionDataDirty = false;
mutable bool _actionDataNeedsTransmit = false;
mutable bool _actionDataDirty { false };
mutable bool _actionDataNeedsTransmit { false };
// _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag
static quint64 _rememberDeletedActionTime;
mutable QHash<QUuid, quint64> _previouslyDeletedActions;
// per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership
mutable bool _hasBidOnSimulation { false };
QUuid _sourceUUID; /// the server node UUID we came from
bool _clientOnly { false };
@ -594,7 +598,7 @@ protected:
// physics related changes from the network to suppress any duplicates and make
// sure redundant applications are idempotent
glm::vec3 _lastUpdatedPositionValue;
glm::quat _lastUpdatedRotationValue;
glm::quat _lastUpdatedRotationValue;
glm::vec3 _lastUpdatedVelocityValue;
glm::vec3 _lastUpdatedAngularVelocityValue;
glm::vec3 _lastUpdatedAccelerationValue;

View file

@ -231,6 +231,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
// and make note of it now, so we can act on it right away.
propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
entity->rememberHasSimulationOwnershipBid();
}
entity->setLastBroadcast(usecTimestampNow());
@ -444,6 +445,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
// we make a bid for simulation ownership
properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY);
entity->pokeSimulationOwnership();
entity->rememberHasSimulationOwnershipBid();
}
}
if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) {

View file

@ -64,7 +64,7 @@ EntityTree::~EntityTree() {
}
void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) {
_entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',');
_entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts);
}
@ -1111,7 +1111,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (message.getType() == PacketType::EntityAdd) {
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
bool failedAdd = !allowed;
if (!allowed) {
qCDebug(entities) << "Filtered entity add. ID:" << entityItemID;
} else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) {
failedAdd = true;
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
<< "] attempted to add an entity ID:" << entityItemID;
} else {
// this is a new entity... assign a new entityID
properties.setCreated(properties.getLastEdited());
properties.setLastEditedBy(senderNode->getUUID());
@ -1126,7 +1134,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
startLogging = usecTimestampNow();
if (wantEditLogging()) {
qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:"
<< newEntity->getEntityItemID();
<< newEntity->getEntityItemID();
qCDebug(entities) << " properties:" << properties;
}
if (wantTerseEditLogging()) {
@ -1136,10 +1144,14 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
}
endLogging = usecTimestampNow();
} else {
failedAdd = true;
qCDebug(entities) << "Add entity failed ID:" << entityItemID;
}
} else {
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
<< "] attempted to add an entity.";
}
if (failedAdd) { // Let client know it failed, so that they don't have an entity that no one else sees.
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
_recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID);
}
} else {
static QString repeatedMessage =

View file

@ -73,6 +73,9 @@ void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) {
_mousePressTime = usecTimestampNow();
_mouseMoved = false;
_mousePressPos = event->pos();
_clickDeadspotActive = true;
eraseMouseClicked();
}
@ -84,9 +87,11 @@ void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) {
// input for this button we might want to add some small tolerance to this so if you do a small drag it
// still counts as a click.
static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click
if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) {
if (_clickDeadspotActive && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) {
_inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel());
}
_clickDeadspotActive = false;
}
void KeyboardMouseDevice::eraseMouseClicked() {
@ -109,9 +114,14 @@ void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) {
// outside of the application window, because we don't get MouseEvents when the cursor is outside
// of the application window.
_lastCursor = currentPos;
_mouseMoved = true;
eraseMouseClicked();
const int CLICK_EVENT_DEADSPOT = 6; // pixels
if (_clickDeadspotActive && (_mousePressPos - currentPos).manhattanLength() > CLICK_EVENT_DEADSPOT) {
eraseMouseClicked();
_clickDeadspotActive = false;
}
}
void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) {

View file

@ -118,8 +118,10 @@ public:
protected:
QPoint _lastCursor;
QPoint _mousePressPos;
quint64 _mousePressTime;
bool _mouseMoved;
bool _clickDeadspotActive;
glm::vec2 _lastTouch;
std::shared_ptr<InputDevice> _inputDevice { std::make_shared<InputDevice>() };

View file

@ -582,6 +582,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_
_nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS;
// copy _outgoingPriority into pendingPriority...
_entity->setPendingOwnershipPriority(_outgoingPriority, now);
// don't forget to remember that we have made a bid
_entity->rememberHasSimulationOwnershipBid();
// ...then reset _outgoingPriority in preparation for the next frame
_outgoingPriority = 0;
} else if (_outgoingPriority != _entity->getSimulationPriority()) {

View file

@ -78,11 +78,12 @@ void initCollisionMaterials() {
}
}
Model::Model(RigPointer rig, QObject* parent) :
Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestableOverride) :
QObject(parent),
_renderGeometry(),
_collisionGeometry(),
_renderWatcher(_renderGeometry),
_spatiallyNestableOverride(spatiallyNestableOverride),
_translation(0.0f),
_rotation(),
_scale(1.0f, 1.0f, 1.0f),
@ -133,16 +134,10 @@ void Model::setRotation(const glm::quat& rotation) {
updateRenderItems();
}
void Model::setSpatiallyNestableOverride(SpatiallyNestablePointer override) {
_spatiallyNestableOverride = override;
updateRenderItems();
}
Transform Model::getTransform() const {
SpatiallyNestablePointer spatiallyNestableOverride = _spatiallyNestableOverride.lock();
if (spatiallyNestableOverride) {
if (_spatiallyNestableOverride) {
bool success;
Transform transform = spatiallyNestableOverride->getTransform(success);
Transform transform = _spatiallyNestableOverride->getTransform(success);
if (success) {
transform.setScale(getScale());
return transform;

View file

@ -67,7 +67,7 @@ public:
static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; }
Model(RigPointer rig, QObject* parent = nullptr);
Model(RigPointer rig, QObject* parent = nullptr, SpatiallyNestable* spatiallyNestableOverride = nullptr);
virtual ~Model();
inline ModelPointer getThisPointer() const {
@ -205,7 +205,6 @@ public:
void setTranslation(const glm::vec3& translation);
void setRotation(const glm::quat& rotation);
void setSpatiallyNestableOverride(SpatiallyNestablePointer ptr);
const glm::vec3& getTranslation() const { return _translation; }
const glm::quat& getRotation() const { return _rotation; }
@ -292,12 +291,11 @@ protected:
GeometryResourceWatcher _renderWatcher;
SpatiallyNestable* _spatiallyNestableOverride;
glm::vec3 _translation;
glm::quat _rotation;
glm::vec3 _scale;
SpatiallyNestableWeakPointer _spatiallyNestableOverride;
glm::vec3 _offset;
static float FAKE_DIMENSION_PLACEHOLDER;

View file

@ -119,7 +119,7 @@ public:
* @param msg {object|string}
*/
Q_INVOKABLE void emitScriptEvent(QVariant msg);
Q_INVOKABLE bool onHomeScreen();
QObject* getTabletSurface();
@ -170,14 +170,14 @@ public:
/**jsdoc
* Returns the current value of this button's properties
* @function TabletButtonProxy#getProperties
* @returns {object}
* @returns {ButtonProperties}
*/
Q_INVOKABLE QVariantMap getProperties() const;
/**jsdoc
* Replace the values of some of this button's properties
* @function TabletButtonProxy#editProperties
* @param properties {object} set of properties to change
* @param {ButtonProperties} properties - set of properties to change
*/
Q_INVOKABLE void editProperties(QVariantMap properties);
@ -199,4 +199,13 @@ protected:
QVariantMap _properties;
};
/**jsdoc
* @typedef TabletButtonProxy.ButtonProperties
* @property {string} text - button caption
* @property {string} icon - url to button icon. (50 x 50)
* @property {string} activeText - button caption when button is active
* @property {string} activeIcon - url to button icon used when button is active. (50 x 50)
* @property {string} isActive - true when button is active.
*/
#endif // hifi_TabletScriptingInterface_h

View file

@ -24,7 +24,7 @@ var DEFAULT_SCRIPTS = [
"system/goto.js",
"system/marketplaces/marketplaces.js",
"system/edit.js",
"system/users.js",
"system/tablet-users.js",
"system/selectAudioDevice.js",
"system/notifications.js",
"system/controllers/controllerDisplayManager.js",

View file

@ -853,7 +853,7 @@ function MyController(hand) {
};
this.setState = function(newState, reason) {
if (isInEditMode() && (newState !== STATE_OFF &&
if ((isInEditMode() && this.grabbedEntity !== HMD.tabletID )&& (newState !== STATE_OFF &&
newState !== STATE_SEARCHING &&
newState !== STATE_OVERLAY_STYLUS_TOUCHING)) {
return;
@ -1703,7 +1703,7 @@ function MyController(hand) {
};
this.isTablet = function (entityID) {
if (entityID === HMD.tabletID) { // XXX what's a better way to know this?
if (entityID === HMD.tabletID) {
return true;
}
return false;

View file

@ -0,0 +1,465 @@
<!--
// users.html
//
// Created by Faye Li on 18 Jan 2017
// Copyright 2017 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
-->
<html>
<head>
<title>Users Online</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600,700"" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style>
body {
margin: 0;
width: 100%;
font-family: 'Raleway', sans-serif;
color: white;
background: linear-gradient(#2b2b2b, #0f212e);
}
.top-bar {
width: 100%;
height: 90px;
background: linear-gradient(#2b2b2b, #1e1e1e);
font-weight: bold;
}
.top-bar .myContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 30px;
margin-right: 30px;
height: 100%;
}
#refresh-button {
width: 24px;
height: 24px;
}
.main {
padding: 30px;
}
#user-info-div {
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 40px;
}
#visibility-toggle {
font-family: 'Raleway';
font-weight: bold;
font-size: 13px;
text-transform: uppercase;
vertical-align: top;
height: 28px;
min-width: 120px;
padding: 0px 18px;
margin-right: 0px;
border-radius: 5px;
border: none;
color: #121212;
background-color: #afafaf;
background: linear-gradient(#fff 20%, #afafaf 100%);
cursor: pointer;
}
#visibility-toggle:enabled:hover {
background: linear-gradient(#fff, #fff);
border: none;
}
#visibility-toggle:active {
background: linear-gradient(#afafaf, #afafaf);
}
#visibility-toggle span {
padding-left: 10px;
}
.tabs {
list-style: none;
padding: 0;
margin: 0;
}
.tabs li {
display: inline-block;
padding: 10px 15px;
}
.tabs li.current {
background: rgba(255,255,255,0.15);
}
.tab-content {
display: none;
}
.tab-content.current {
display: inherit;
background: rgba(255,255,255,0.15);
}
.tab-content ul {
list-style: none;
padding: 15px 0px 15px 15px;
margin: 0;
}
.tab-content ul li {
padding: 2px 0px;
}
input[type=button] {
font-family: 'Raleway';
font-weight: bold;
font-size: 13px;
text-transform: uppercase;
vertical-align: top;
height: 28px;
min-width: 120px;
padding: 0px 18px;
margin-right: 6px;
border-radius: 5px;
border: none;
color: #fff;
background-color: #000;
background: linear-gradient(#343434 20%, #000 100%);
cursor: pointer;
}
input[type=button].red {
color: #fff;
background-color: #94132e;
background: linear-gradient(#d42043 20%, #94132e 100%);
}
input[type=button].blue {
color: #fff;
background-color: #1080b8;
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
}
input[type=button].white {
color: #121212;
background-color: #afafaf;
background: linear-gradient(#fff 20%, #afafaf 100%);
}
input[type=button]:enabled:hover {
background: linear-gradient(#000, #000);
border: none;
}
input[type=button].red:enabled:hover {
background: linear-gradient(#d42043, #d42043);
border: none;
}
input[type=button].blue:enabled:hover {
background: linear-gradient(#00b4ef, #00b4ef);
border: none;
}
input[type=button].white:enabled:hover {
background: linear-gradient(#fff, #fff);
border: none;
}
input[type=button]:active {
background: linear-gradient(#343434, #343434);
}
input[type=button].red:active {
background: linear-gradient(#94132e, #94132e);
}
input[type=button].blue:active {
background: linear-gradient(#1080b8, #1080b8);
}
input[type=button].white:active {
background: linear-gradient(#afafaf, #afafaf);
}
input[type=button]:disabled {
color: #252525;
background: linear-gradient(#575757 20%, #252525 100%);
}
input[type=button][pressed=pressed] {
color: #00b4ef;
}
#friends-button {
margin: 0px 0px 15px 10px;
}
/*Vertically Center Modal*/
.modal {
color: black;
text-align: center;
padding: 0!important;
}
.modal:before {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
margin-right: -4px;
}
.modal-dialog {
display: inline-block;
text-align: left;
vertical-align: middle;
}
.dropdown-menu {
width: 280px;
}
.dropdown-menu li {
color: #333;
padding: 10px;
}
.dropdown-menu .divider {
padding: 0px;
}
.dropdown-menu li:hover {
background: #dcdcdc;
}
.dropdown-menu li h6 {
font-weight: bold;
}
.dropdown-menu li p {
font-size: 11px;
}
</style>
</head>
<body>
<div class="top-bar">
<div class="myContainer">
<div>Users Online</div>
<img id="refresh-button" onclick="pollUsers()" src="https://hifi-content.s3.amazonaws.com/faye/tablet-dev/refresh-icon.svg"></img>
</div>
</div>
<div class="main">
<div id="user-info-div">
<h4></h4>
<div class="dropdown">
<button id="visibility-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Online
<span class="glyphicon glyphicon-menu-down"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="visibility-toggle">
<li class="visibility-option" data-visibility="all">
<h6>Online</h6>
<p>You will be shown online to everyone else. Anybody will be able to find you from the users online list and jump to your current location.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="friends">
<h6>Available to Friends Only</h6>
<p>You will be shown online only to users you have added as friends. Other users may still interact with you in the same domain, but they won't be able to find you from the users online list.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="none">
<h6>Appear Offline</h6>
<p>No one will be able to find you from the users online list. However, this does not prevent other users in the same domain from interacting with you. For a complete "Do not disturb" mode, you may want to go to your own private domain and set allow entering to no one.</p>
</li>
</ul>
</div>
</div>
<ul class="tabs">
<li tab-id="tab-1" class="current">Everyone (0)</li>
<li tab-id="tab-2">Friends (0)</li>
</ul>
<div id="tab-1" class="tab-content current">
<ul></ul>
</div>
<div id="tab-2" class="tab-content">
<ul></ul>
<input type="button" class="blue" id="friends-button" value="Add/Remove Friends">
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Jump to username @ Placename</h4>
</div>
<div class="modal-body">
This will teleport you to a new location and possibly another domain. Are you sure?
</div>
<div class="modal-footer">
<input type="button" data-dismiss="modal" value="Cancel">
<input type="button" data-dismiss="modal" id="jump-to-confirm-button" value="OK">
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script>
var METAVERSE_API_URL = "https://metaverse.highfidelity.com/api/v1/users?status=online";
var FRIENDS_FILTER = "&filter=friends";
var myUsername = null;
var myVisibility = null;
function displayUsers(data, element) {
element.empty();
for (var i = 0; i < data.users.length; i++) {
// Don't display users who aren't in a domain
if (typeof data.users[i].location.root.name === "undefined") {
console.log(data.users[i].username + "is online but not in a domain");
$("#dev-div").append("<p>" + data.users[i].username + "is online but not in a domain</p>");
} else {
$("#dev-div").append("<li>" + data.users[i].username + " @ " + data.users[i].location.root.name + "</li>");
if (data.users[i].username === myUsername) {
$("#user-info-div h4").text(data.users[i].username + " @ " + data.users[i].location.root.name);
} else {
console.log(data.users[i].username + " @ " + data.users[i].location.root.name);
// Create a list item and put user info in data-* attributes, also make it trigger the jump to confirmation modal
$("<li></li>", {
"data-toggle": "modal",
"data-target": "#myModal",
"data-username": data.users[i].username,
"data-placename": data.users[i].location.root.name,
text: data.users[i].username + " @ " + data.users[i].location.root.name
}).appendTo(element);
}
}
}
}
function processData(data, type) {
var num = data.users.length;
if (type === "everyone") {
$(".tabs li:nth-child(1)").text("Everyone (" + num + ")");
displayUsers(data, $("#tab-1 ul"));
} else if (type === "friends") {
$(".tabs li:nth-child(2)").text("Friends (" + num + ")");
displayUsers(data, $("#tab-2 ul"));
}
}
function pollUsers() {
$("#dev-div").append("<p>polling users..</p>");
$.ajax({
url: METAVERSE_API_URL,
success: function(response) {
console.log(response);
$("#dev-div").append("<p>polling everyone sucess</p>");
processData(response.data, "everyone");
}
});
$.ajax({
url: METAVERSE_API_URL + FRIENDS_FILTER,
success: function(response) {
console.log(response);
$("#dev-div").append("<p>polling friends sucess</p>");
processData(response.data, "friends");
}
});
}
function onScriptEventReceived(event) {
$("#dev-div").append("<p>Received a script event, its type is " + typeof event + "</p>");
if (typeof event === "string") {
// Parse the string into an object
event = JSON.parse(event);
}
if (event.type === "user-info") {
myUsername = event.data.username;
myVisibility = event.data.visibility;
$("#user-info-div h4").text(myUsername);
var buttonText = "Online";
if (myVisibility === "none") {
buttonText = "Appear Offline";
} else if (myVisibility === "friends") {
buttonText = "Available To Friends Only";
}
$("#visibility-toggle").html(buttonText + "<span class='glyphicon glyphicon-menu-down'></span>")
}
}
$(document).ready(function() {
$("#dev-div").append("<p>ready</p>");
// Auto-load user lists when page loads
pollUsers();
// Click listener for tabs
$(".tabs li").click(function() {
var tabID = $(this).attr("tab-id");
$(".tab-content").removeClass("current");
$("#" + tabID).addClass("current");
$(".tabs li").removeClass("current");
$(this).addClass("current");
});
// Jump to user: Fill confirmation modal with data of the selected user
$("#myModal").on("show.bs.modal", function (event) {
// Get the element that triggered the modal
var li = $(event.relatedTarget);
// Extract info from data-* attributes
var username = li.data("username");
var placename = li.data("placename");
// Write info to the modal
var modal = $(this);
modal.find(".modal-title").text("Jump to " + username + " @ " + placename);
$("#jump-to-confirm-button").data("username", username);
})
$("#jump-to-confirm-button").click(function() {
var jumpToObject = {
"type": "jump-to",
"data": {
"username": $(this).data("username")
}
}
EventBridge.emitWebEvent(JSON.stringify(jumpToObject));
});
// Click listener for toggling who can see me
$(".visibility-option").click(function() {
myVisibility = $(this).data("visibility");
var newButtonText = $(this).find("h6").text();
$("#visibility-toggle").html(newButtonText + "<span class='glyphicon glyphicon-menu-down'></span>");
var visibilityObject = {
"type": "toggle-visibility",
"data": {
"visibility": myVisibility
}
}
EventBridge.emitWebEvent(JSON.stringify(visibilityObject));
});
// Listen for events from hifi
EventBridge.scriptEventReceived.connect(onScriptEventReceived);
// Send a ready event to hifi
var eventObject = {"type": "ready"};
EventBridge.emitWebEvent(JSON.stringify(eventObject));
// Click listener for add/remove friends button
$("#friends-button").click(function() {
// Send a manage friends event to hifi
eventObject = {"type": "manage-friends"};
EventBridge.emitWebEvent(JSON.stringify(eventObject));
});
});
</script>
</body>
</html>

View file

@ -24,6 +24,7 @@ var CAMERA_MATRIX = -7;
var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0};
var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 };
var INCHES_TO_METERS = 1 / 39.3701;
var NO_HANDS = -1;
var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx";
@ -35,18 +36,21 @@ var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-wi
// * position - position in front of the user
// * rotation - rotation of entity so it faces the user.
function calcSpawnInfo(hand, height) {
var noHands = -1;
var finalPosition;
if (HMD.active && hand !== noHands) {
var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position;
var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation;
if (HMD.active && hand !== NO_HANDS) {
var handController = getControllerWorldLocation(hand, true);
var controllerPosition = handController.position;
// compute the angle of the chord with length (height / 2)
var theta = Math.asin(height / (2 * Vec3.distance(HMD.position, controllerPosition)));
var theta = Math.asin(height / (2 * Vec3.distance(headPos, controllerPosition)));
// then we can use this angle to rotate the vector between the HMD position and the center of the tablet.
// this vector, u, will become our new look at direction.
var d = Vec3.normalize(Vec3.subtract(HMD.position, controllerPosition));
var d = Vec3.normalize(Vec3.subtract(headPos, controllerPosition));
var w = Vec3.normalize(Vec3.cross(Y_AXIS, d));
var q = Quat.angleAxis(theta * (180 / Math.PI), w);
var u = Vec3.multiplyQbyV(q, d);
@ -64,8 +68,8 @@ function calcSpawnInfo(hand, height) {
rotation: lookAtRot
};
} else {
var front = Quat.getFront(Camera.orientation);
finalPosition = Vec3.sum(Camera.position, Vec3.multiply(0.6, front));
var front = Quat.getFront(headRot);
finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front));
var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0});
return {
position: finalPosition,
@ -198,6 +202,11 @@ WebTablet = function (url, width, dpi, hand, clientOnly) {
_this.geometryChanged(geometry);
};
Window.geometryChanged.connect(this.myGeometryChanged);
this.myCameraModeChanged = function(newMode) {
_this.cameraModeChanged(newMode);
};
Camera.modeUpdated.connect(this.myCameraModeChanged);
};
WebTablet.prototype.setHomeButtonTexture = function() {
@ -228,11 +237,11 @@ WebTablet.prototype.destroy = function () {
Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent);
Window.geometryChanged.disconnect(this.myGeometryChanged);
Camera.modeUpdated.disconnect(this.myCameraModeChanged);
};
WebTablet.prototype.geometryChanged = function (geometry) {
if (!HMD.active) {
var NO_HANDS = -1;
var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
@ -288,7 +297,6 @@ WebTablet.prototype.onHmdChanged = function () {
Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent);
}
var NO_HANDS = -1;
var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet
this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
@ -370,6 +378,18 @@ WebTablet.prototype.mousePressEvent = function (event) {
}
};
WebTablet.prototype.cameraModeChanged = function (newMode) {
// reposition the tablet.
// This allows HMD.position to reflect the new camera mode.
if (HMD.active) {
var self = this;
var tabletProperties = {};
// compute position, rotation & parentJointIndex of the tablet
self.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties);
Entities.editEntity(self.tabletEntityID, tabletProperties);
}
};
function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {

View file

@ -1038,7 +1038,7 @@ SelectionDisplay = (function() {
if (entityIntersection.intersects &&
(!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) {
if (HMD.tabletID == entityIntersection.entityID) {
if (HMD.tabletID === entityIntersection.entityID) {
return;
}

View file

@ -0,0 +1,98 @@
"use strict";
//
// users.js
//
// Created by Faye Li on 18 Jan 2017.
// Copyright 2017 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() { // BEGIN LOCAL_SCOPE
var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html";
var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends";
var FRIENDS_WINDOW_WIDTH = 290;
var FRIENDS_WINDOW_HEIGHT = 500;
var FRIENDS_WINDOW_TITLE = "Add/Remove Friends";
// Initialise visibility based on global service
var VISIBILITY_VALUES_SET = {};
VISIBILITY_VALUES_SET["all"] = true;
VISIBILITY_VALUES_SET["friends"] = true;
VISIBILITY_VALUES_SET["none"] = true;
var myVisibility;
if (GlobalServices.findableBy in VISIBILITY_VALUES_SET) {
myVisibility = GlobalServices.findableBy;
} else {
// default to friends if it can't be determined
myVisibility = "friends";
GlobalServices.findableBy = myVisibilty;
}
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var button = tablet.addButton({
icon: "icons/tablet-icons/people-i.svg",
text: "Users"
});
function onClicked() {
tablet.gotoWebScreen(USERS_URL);
}
function onWebEventReceived(event) {
print("Script received a web event, its type is " + typeof event);
if (typeof event === "string") {
event = JSON.parse(event);
}
if (event.type === "ready") {
// send username to html
var myUsername = GlobalServices.username;
var object = {
"type": "user-info",
"data": {
"username": myUsername,
"visibility": myVisibility
}
};
tablet.emitScriptEvent(JSON.stringify(object));
}
if (event.type === "manage-friends") {
// open a web overlay to metaverse friends page
var friendsWindow = new OverlayWebWindow({
title: FRIENDS_WINDOW_TITLE,
width: FRIENDS_WINDOW_WIDTH,
height: FRIENDS_WINDOW_HEIGHT,
visible: false
});
friendsWindow.setURL(FRIENDS_WINDOW_URL);
friendsWindow.setVisible(true);
friendsWindow.raise();
}
if (event.type === "jump-to") {
if (typeof event.data.username !== undefined) {
// teleport to selected user from the online users list
location.goToUser(event.data.username);
}
}
if (event.type === "toggle-visibility") {
if (typeof event.data.visibility !== undefined) {
// update your visibility (all, friends, or none)
myVisibility = event.data.visibility;
GlobalServices.findableBy = myVisibility;
}
}
}
button.clicked.connect(onClicked);
tablet.webEventReceived.connect(onWebEventReceived);
function cleanup() {
button.clicked.disconnect(onClicked);
tablet.removeButton(button);
}
Script.scriptEnding.connect(cleanup);
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,21 @@
(function() {
var MINIMUM_LIGHT_INTENSITY = 100.0;
var MAXIMUM_LIGHT_INTENSITY = 125.0;
// Return a random number between `low` (inclusive) and `high` (exclusive)
function randFloat(low, high) {
return low + Math.random() * (high - low);
}
var self = this;
this.preload = function(entityID) {
self.intervalID = Script.setInterval(function() {
Entities.editEntity(entityID, {
intensity: randFloat(MINIMUM_LIGHT_INTENSITY, MAXIMUM_LIGHT_INTENSITY)
});
}, 100);
};
this.unload = function() {
Script.clearInterval(self.intervalID);
}
});