diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 3b1aff0fd5..827ec97b07 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -36,11 +36,11 @@ Rectangle { property bool isAvatarInFavorites: currentAvatar ? allAvatars.findAvatar(currentAvatar.name) !== undefined : false property int avatarWearablesCount: currentAvatar ? currentAvatar.wearables.count : 0 property var currentAvatar: null; - function setCurrentAvatar(avatar) { + function setCurrentAvatar(avatar, bookmarkName) { var avatarThumbnailUrl = allAvatars.makeThumbnailUrl(avatar.avatarUrl); var currentAvatarObject = { - 'name' : '', + 'name' : bookmarkName, 'scale' : avatar.avatarScale, 'url' : avatarThumbnailUrl, 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], @@ -63,7 +63,7 @@ Rectangle { } else if(message.method === 'wearableUpdated') { adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties); } else if(message.method === 'wearablesUpdated') { - var wearablesModel = currentAvatarModel.get(0).wearables; + var wearablesModel = currentAvatar.wearables; console.debug('handling wearablesUpdated, new wearables count:', message.wearables.length, ': old wearables: '); for(var i = 0; i < wearablesModel.count; ++i) { @@ -82,11 +82,13 @@ Rectangle { adjustWearables.refresh(currentAvatar); } else if(message.method === 'bookmarkLoaded') { - setCurrentAvatar(message.reply.currentAvatar); - selectedAvatarName = message.reply.name; - var avatarIndex = allAvatars.findAvatarIndex(selectedAvatarName); + setCurrentAvatar(message.reply.currentAvatar, message.reply.name); + var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name); allAvatars.move(avatarIndex, 0, 1); view.setPage(0); + } else if(message.method === 'bookmarkAdded') { + var avatarIndex = allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName); + updateCurrentAvatarInBookmarks(currentAvatar); } else if(message.method === 'bookmarkDeleted') { pageOfAvatars.isUpdating = true; @@ -115,10 +117,12 @@ Rectangle { } else if(message.method === getAvatarsMethod) { var getAvatarsReply = message.reply; allAvatars.populate(getAvatarsReply.bookmarks); - setCurrentAvatar(getAvatarsReply.currentAvatar); + setCurrentAvatar(getAvatarsReply.currentAvatar, ''); console.debug('currentAvatar: ', JSON.stringify(currentAvatar, null, '\t')); updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'updateAvatarInBookmarks') { + updateCurrentAvatarInBookmarks(currentAvatar); } else if(message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); } @@ -130,7 +134,6 @@ Rectangle { if(bookmarkAvatarIndex === -1) { console.debug('bookmarkAvatarIndex = -1, avatar is not favorite') avatar.name = ''; - selectedAvatarName = ''; view.setPage(0); } else { console.debug('bookmarkAvatarIndex = ', bookmarkAvatarIndex, 'avatar is among favorites!') @@ -140,11 +143,6 @@ Rectangle { } } - property string selectedAvatarName: '' - onSelectedAvatarNameChanged: { - console.debug('selectedAvatarId: ', selectedAvatarName) - } - property bool isInManageState: false Component.onCompleted: { @@ -217,9 +215,6 @@ Rectangle { } onAdjustWearablesClosed: { emitSendToScript({'method' : 'adjustWearablesClosed', 'save' : status, 'avatarName' : avatarName}); - if(status) { - updateCurrentAvatarInBookmarks(currentAvatar); - } } onWearableSelected: { emitSendToScript({'method' : 'selectWearable', 'entityID' : id}); @@ -356,14 +351,11 @@ Rectangle { } entry.avatarEntites = wearables; + currentAvatar.name = createFavorite.favoriteNameText; + console.debug('became: ', JSON.stringify(entry, 0, 4)); - /* - var newAvatar = JSON.parse(JSON.stringify(currentAvatar)); - newAvatar.name = createFavorite.favoriteNameText; - allAvatars.append(newAvatar); - view.selectAvatar(newAvatar); - */ + emitSendToScript({'method': 'addAvatar', 'name' : currentAvatar.name}); createFavorite.close(); } @@ -523,7 +515,11 @@ Rectangle { id: view anchors.fill: parent interactive: false; - currentIndex: (selectedAvatarName !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatarIndex(selectedAvatarName) : -1 + currentIndex: currentAvatarIndexInBookmarksPage(); + + function currentAvatarIndexInBookmarksPage() { + return (currentAvatar && currentAvatar.name !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatarIndex(currentAvatar.name) : -1; + } property int horizontalSpacing: 18 property int verticalSpacing: 44 @@ -549,9 +545,7 @@ Rectangle { property int currentPage: 0; onCurrentPageChanged: { console.debug('currentPage: ', currentPage) - currentIndex = Qt.binding(function() { - return (selectedAvatarName !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatar(selectedAvatarName) : -1 - }) + currentIndex = Qt.binding(currentAvatarIndexInBookmarksPage); } property bool hasNext: currentPage < (totalPages - 1) diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 810fce7f6d..f1b3e4c1b1 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -15,8 +15,8 @@ Rectangle { signal wearableSelected(var id); signal wearableDeleted(string avatarName, var id); - signal adjustWearablesOpened(); - signal adjustWearablesClosed(bool status); + signal adjustWearablesOpened(var avatarName); + signal adjustWearablesClosed(bool status, var avatarName); property bool modified: false; Component.onCompleted: { @@ -32,9 +32,10 @@ Rectangle { property var wearablesModel; function open(avatar) { - adjustWearablesOpened(); console.debug('AdjustWearables.qml: open: ', JSON.stringify(avatar, null, '\t')); + adjustWearablesOpened(avatar.name); + visible = true; avatarName = avatar.name; wearablesModel = avatar.wearables; @@ -103,7 +104,7 @@ Rectangle { function close(status) { visible = false; - adjustWearablesClosed(status); + adjustWearablesClosed(status, avatarName); } HifiConstants { id: hifi } diff --git a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml index 8b936b0a8e..c31bc8366e 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml @@ -16,20 +16,35 @@ ListModel { return avatarThumbnailUrl; } + function makeAvatarEntry(avatar, avatarName) { + console.debug('makeAvatarEntry: ', avatarName, JSON.stringify(avatar)); + var avatarThumbnailUrl = makeThumbnailUrl(avatar.avatarUrl); + + return { + 'name' : avatarName, + 'scale' : avatar.avatarScale, + 'url' : avatarThumbnailUrl, + 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], + 'attachments' : avatar.attachments ? avatar.attachments : [], + 'entry' : avatar, + 'getMoreAvatars' : false + }; + + } + + function addAvatarEntry(avatar, avatarName) { + console.debug('addAvatarEntry: ', avatarName); + + var avatarEntry = makeAvatarEntry(avatar, avatarName); + append(avatarEntry); + + return allAvatars.count - 1; + } + function populate(bookmarks) { for(var avatarName in bookmarks) { var avatar = bookmarks[avatarName]; - var avatarThumbnailUrl = makeThumbnailUrl(avatar.avatarUrl); - - var avatarEntry = { - 'name' : avatarName, - 'scale' : avatar.avatarScale, - 'url' : avatarThumbnailUrl, - 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], - 'attachments' : avatar.attachments ? avatar.attachments : [], - 'entry' : avatar, - 'getMoreAvatars' : false - }; + var avatarEntry = makeAvatarEntry(avatar, avatarName); append(avatarEntry); } @@ -62,11 +77,20 @@ ListModel { for(var i = 0; i < m1.count; ++i) { var e1 = m1.get(i); - var e2 = m2.get(i); - console.debug('comparing ', JSON.stringify(e1), JSON.stringify(e2)); + var allDifferent = true; - if(!comparer(e1, e2)) { + // it turns out order of wearables can randomly change so make position-independent comparison here + for(var j = 0; j < m2.count; ++j) { + var e2 = m2.get(j); + + if(comparer(e1, e2)) { + allDifferent = false; + break; + } + } + + if(allDifferent) { return false; } } @@ -74,10 +98,47 @@ ListModel { return true; } - function compareArrays(a1, a2, props) { - for(var prop in props) { - if(JSON.stringify(a1[prop]) !== JSON.stringify(a2[prop])) { + function compareNumericObjects(o1, o2) { + if(o1 === undefined && o2 !== undefined) + return false; + if(o1 !== undefined && o2 === undefined) + return false; + + for(var prop in o1) { + var v1 = o1[prop]; + var v2 = o2[prop]; + + if(v1 !== v2 && Math.round(v1 * 1000) != Math.round(v2 * 1000)) return false; + } + + return true; + } + + function compareObjects(o1, o2, props, arrayProp) { + + console.debug('compare ojects: o1 = ', JSON.stringify(o1, null, 4), 'o2 = ', JSON.stringify(o2, null, 4)); + for(var i = 0; i < props.length; ++i) { + var prop = props[i]; + var propertyName = prop.propertyName; + var comparer = prop.comparer; + + var o1Value = arrayProp ? o1[arrayProp][propertyName] : o1[propertyName]; + var o2Value = arrayProp ? o2[arrayProp][propertyName] : o2[propertyName]; + + console.debug('compare values for key: ', propertyName, comparer ? 'using specified comparer' : 'using default comparer', ', o1 = ', JSON.stringify(o1Value, null, 4), 'o2 = ', JSON.stringify(o2Value, null, 4)); + + if(comparer) { + if(comparer(o1Value, o2Value) === false) { + console.debug('not equal'); + return false; + } else { + console.debug('equal'); + } + } else { + if(JSON.stringify(o1Value) !== JSON.stringify(o2Value)) { + return false; + } } } @@ -85,11 +146,22 @@ ListModel { } function compareWearables(w1, w2) { - return compareArrays(w1, w2, ['modelUrl', 'parentJointIndex', 'marketplaceID', 'itemName', 'script', 'rotation']) + return compareObjects(w1, w2, [{'propertyName' : 'modelURL'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'marketplaceID'}, + {'propertyName' : 'itemName'}, + {'propertyName' : 'script'}, + {'propertyName' : 'rotation', 'comparer' : compareNumericObjects}, + {'propertyName' : 'localPosition', 'comparer' : compareNumericObjects}, + {'propertyName' : 'localRotation', 'comparer' : compareNumericObjects}, + {'propertyName' : 'dimensions', 'comparer' : compareNumericObjects}], 'properties') } function compareAttachments(a1, a2) { - return compareAttachments(a1, a2, ['position', 'orientation', 'parentJointIndex', 'modelurl']) + return compareObjects(a1, a2, [{'propertyName' : 'position', 'comparer' : compareNumericObjects}, + {'propertyName' : 'orientation'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'modelurl'}]) } function findAvatarIndexByValue(avatar) { diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 25d7a472a4..d17fa6889a 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -108,6 +108,8 @@ void AvatarBookmarks::addBookmark(const QString& bookmarkName) { addBookmarkToMenu(menubar, bookmarkName, bookmark); insert(bookmarkName, bookmark); enableMenuItems(true); + + emit bookmarkAdded(bookmarkName); } void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { @@ -212,6 +214,25 @@ void AvatarBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { Bookmarks::sortActions(menubar, _bookmarksMenu); } +QVariantMap AvatarBookmarks::getBookmark(const QString &bookmarkName) +{ + if (QThread::currentThread() != thread()) { + QVariantMap result; + BLOCKING_INVOKE_METHOD(this, "getBookmark", Q_RETURN_ARG(QVariantMap, result), Q_ARG(QString, bookmarkName)); + return result; + } + + QVariantMap bookmark; + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + bookmark = bookmarkEntry.value().toMap(); + } + + return bookmark; +} + void AvatarBookmarks::changeToBookmarkedAvatar() { QAction* action = qobject_cast(sender()); auto myAvatar = DependencyManager::get()->getMyAvatar(); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 8a5a9ce318..bca476195c 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -31,7 +31,7 @@ class AvatarBookmarks: public Bookmarks, public Dependency { public: AvatarBookmarks(); void setupMenus(Menu* menubar, MenuWrapper* menu) override; - + Q_INVOKABLE QVariantMap getBookmark(const QString& bookmarkName); public slots: /**jsdoc @@ -49,6 +49,7 @@ public slots: signals: void bookmarkLoaded(const QString& bookmarkName); void bookmarkDeleted(const QString& bookmarkName); + void bookmarkAdded(const QString& bookmarkName); protected: void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 7ea8fcfa53..c8d7363037 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -25,6 +25,10 @@ var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; var ENTRY_VERSION = "version"; +function executeLater(callback) { + Script.setTimeout(callback, 300); +} + function getMyAvatarWearables() { var wearablesArray = MyAvatar.getAvatarEntitiesVariant(); @@ -70,8 +74,17 @@ function getMyAvatar() { return avatar; } -function updateAvatarWearables(avatar, wearables) { - avatar[ENTRY_AVATAR_ENTITIES] = wearables; +function updateAvatarWearables(avatar, bookmarkAvatarName) { + console.debug('avatarapp.js: scheduling wearablesUpdated notify for', bookmarkAvatarName); + + executeLater(function() { + console.debug('avatarapp.js: executing wearablesUpdated notify for', bookmarkAvatarName); + + var wearables = getMyAvatarWearables(); + avatar[ENTRY_AVATAR_ENTITIES] = wearables; + + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : bookmarkAvatarName}) + }); } var adjustWearables = { @@ -132,19 +145,30 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See console.debug('avatarapp.js: deleting avatar: ', message.name); AvatarBookmarks.removeBookmark(message.name); break; + case 'addAvatar': + console.debug('avatarapp.js: saving avatar: ', message.name); + AvatarBookmarks.addBookmark(message.name); + break; case 'adjustWearablesOpened': + console.debug('avatarapp.js: adjustWearablesOpened'); + currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); Entities.mousePressOnEntity.connect(onSelectedEntity); break; case 'adjustWearablesClosed': + console.debug('avatarapp.js: adjustWearablesClosed'); + if(!message.save) { // revert changes using snapshot of wearables console.debug('reverting... '); if(currentAvatarWearablesBackup !== null) { AvatarBookmarks.updateAvatarEntities(currentAvatarWearablesBackup); - sendToQml({'method' : 'wearablesUpdated', 'wearables' : getMyAvatarWearables(), 'avatarName' : message.avatarName}); + updateAvatarWearables(currentAvatar, message.avatarName); } + } else { + console.debug('saving... '); + sendToQml({'method' : 'updateAvatarInBookmarks'}); } adjustWearables.setOpened(false); @@ -155,18 +179,9 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See ensureWearableSelected(message.entityID); break; case 'deleteWearable': - console.debug('before deleting: wearables.length: ', getMyAvatarWearables().length); - console.debug(JSON.stringify(Entities.getEntityProperties(message.entityID, ['parentID']))); - - Entities.editEntity(message.entityID, { parentID : null }) // 2do: remove this hack when backend will be fixed - console.debug(JSON.stringify(Entities.getEntityProperties(message.entityID, ['parentID']))); - + console.debug('avatarapp.js: deleteWearable: ', message.entityID, 'from avatar: ', message.avatarName); Entities.deleteEntity(message.entityID); - var wearables = getMyAvatarWearables(); - console.debug('after deleting: wearables.length: ', wearables.length); - - updateAvatarWearables(currentAvatar, wearables); - sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : message.avatarName}) + updateAvatarWearables(currentAvatar, message.avatarName); break; default: print('Unrecognized message from AvatarApp.qml:', JSON.stringify(message)); @@ -253,9 +268,13 @@ function sendToQml(message) { } function onBookmarkLoaded(bookmarkName) { - currentAvatar = getMyAvatar(); - console.debug('avatarapp.js: onBookmarkLoaded: ', JSON.stringify(currentAvatar, 0, 4)); - sendToQml({'method' : 'bookmarkLoaded', 'reply' : {'name' : bookmarkName, 'currentAvatar' : currentAvatar} }); + console.debug('avatarapp.js: scheduling onBookmarkLoaded: ', bookmarkName); + + executeLater(function() { + currentAvatar = getMyAvatar(); + console.debug('avatarapp.js: executing onBookmarkLoaded: ', JSON.stringify(currentAvatar, 0, 4)); + sendToQml({'method' : 'bookmarkLoaded', 'reply' : {'name' : bookmarkName, 'currentAvatar' : currentAvatar} }); + }); } function onBookmarkDeleted(bookmarkName) { @@ -263,6 +282,11 @@ function onBookmarkDeleted(bookmarkName) { sendToQml({'method' : 'bookmarkDeleted', 'name' : bookmarkName}); } +function onBookmarkAdded(bookmarkName) { + console.debug('avatarapp.js: onBookmarkAdded: ', bookmarkName); + sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': AvatarBookmarks.getBookmark(bookmarkName) }); +} + // // Manage the connection between the button and the window. // @@ -282,6 +306,7 @@ function startup() { tablet.screenChanged.connect(onTabletScreenChanged); AvatarBookmarks.bookmarkLoaded.connect(onBookmarkLoaded); AvatarBookmarks.bookmarkDeleted.connect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.connect(onBookmarkAdded); // Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); // Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); @@ -384,6 +409,7 @@ function shutdown() { tablet.screenChanged.disconnect(onTabletScreenChanged); AvatarBookmarks.bookmarkLoaded.disconnect(onBookmarkLoaded); AvatarBookmarks.bookmarkDeleted.disconnect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.disconnect(onBookmarkAdded); // Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); // Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL);