History

Attachment dialog

Brad Davis committed 10 years ago

Commit 664100b1 parent 881a94c14 files · +774 −1Browse files

Unified · Split

14 files
  1. interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +123 −0
  2. interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml +128 −0
  3. interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +128 −0
  4. interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +164 −0
  5. interface/resources/qml/hifi/dialogs/attachments/Rotation.qml +9 −0
  6. interface/resources/qml/hifi/dialogs/attachments/Translation.qml +9 −0
  7. interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +71 −0
  8. interface/resources/qml/hifi/models/S3Model.qml +41 −0
  9. interface/resources/qml/js/Utils.js +14 −0
  10. interface/src/Application.cpp +5 −0
  11. libraries/avatars/src/AvatarData.cpp +63 −0
  12. libraries/avatars/src/AvatarData.h +6 −0
  13. libraries/networking/src/LimitedNodeList.cpp +1 −1
  14. tests/ui/qml/main.qml +12 −0
interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml+123−0
@@ -0,0 +1,123 @@
1+import QtQuick 2.5
2+import QtQuick.Controls 1.4
3+import Qt.labs.settings 1.0
4+
5+import "../../windows"
6+import "attachments"
7+
8+Window {
9+ id: root
10+ title: "Edit Attachments"
11+ width: 600
12+ height: 600
13+ resizable: true
14+ // User must click OK or cancel to close the window
15+ closable: false
16+
17+ readonly property var originalAttachments: MyAvatar.getAttachmentsVariant();
18+ property var attachments: [];
19+
20+ property var settings: Settings {
21+ category: "AttachmentsDialog"
22+ property alias x: root.x
23+ property alias y: root.y
24+ property alias width: root.width
25+ property alias height: root.height
26+ }
27+
28+ Component.onCompleted: {
29+ for (var i = 0; i < originalAttachments.length; ++i) {
30+ var attachment = originalAttachments[i];
31+ root.attachments.push(attachment);
32+ listView.model.append({});
33+ }
34+ }
35+
36+ Rectangle {
37+ anchors.fill: parent
38+ radius: 4
39+
40+ Rectangle {
41+ id: attachmentsBackground
42+ anchors { left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; margins: 8 }
43+ color: "gray"
44+ radius: 4
45+
46+ ScrollView{
47+ id: scrollView
48+ anchors.fill: parent
49+ anchors.margins: 4
50+ ListView {
51+ id: listView
52+ model: ListModel {}
53+ delegate: Item {
54+ implicitHeight: attachmentView.height + 8;
55+ implicitWidth: attachmentView.width;
56+ Attachment {
57+ id: attachmentView
58+ width: scrollView.width
59+ attachment: root.attachments[index]
60+ onDeleteAttachment: {
61+ attachments.splice(index, 1);
62+ listView.model.remove(index, 1);
63+ }
64+ onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments);
65+ }
66+ }
67+ onCountChanged: MyAvatar.setAttachmentsVariant(attachments);
68+ }
69+ }
70+ }
71+
72+ Button {
73+ id: newAttachmentButton
74+ anchors { left: parent.left; right: parent.right; bottom: buttonRow.top; margins: 8 }
75+ text: "New Attachment"
76+
77+ onClicked: {
78+ var template = {
79+ modelUrl: "",
80+ translation: { x: 0, y: 0, z: 0 },
81+ rotation: { x: 0, y: 0, z: 0 },
82+ scale: 1,
83+ jointName: MyAvatar.jointNames[0],
84+ soft: false
85+ };
86+ attachments.push(template);
87+ listView.model.append({});
88+ MyAvatar.setAttachmentsVariant(attachments);
89+ }
90+ }
91+
92+ Row {
93+ id: buttonRow
94+ spacing: 8
95+ anchors { right: parent.right; bottom: parent.bottom; margins: 8 }
96+ Button { action: cancelAction }
97+ Button { action: okAction }
98+ }
99+
100+ Action {
101+ id: cancelAction
102+ text: "Cancel"
103+ onTriggered: {
104+ MyAvatar.setAttachmentsVariant(originalAttachments);
105+ root.destroy()
106+ }
107+ }
108+
109+ Action {
110+ id: okAction
111+ text: "OK"
112+ onTriggered: {
113+ for (var i = 0; i < attachments.length; ++i) {
114+ console.log("Attachment " + i + ": " + attachments[i]);
115+ }
116+
117+ MyAvatar.setAttachmentsVariant(attachments);
118+ root.destroy()
119+ }
120+ }
121+ }
122+}
123+
interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml+128−0
@@ -0,0 +1,128 @@
1+import QtQuick 2.5
2+import QtQuick.Controls 1.4
3+import QtQuick.XmlListModel 2.0
4+
5+import "../../windows"
6+import "../../js/Utils.js" as Utils
7+import "../models"
8+
9+ModalWindow {
10+ id: root
11+ resizable: true
12+ width: 640
13+ height: 480
14+
15+ property var result;
16+
17+ signal selected(var modelUrl);
18+ signal canceled();
19+
20+ Rectangle {
21+ anchors.fill: parent
22+ color: "white"
23+
24+ Item {
25+ anchors { fill: parent; margins: 8 }
26+
27+ TextField {
28+ id: filterEdit
29+ anchors { left: parent.left; right: parent.right; top: parent.top }
30+ placeholderText: "filter"
31+ onTextChanged: tableView.model.filter = text
32+ }
33+
34+ TableView {
35+ id: tableView
36+ anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 }
37+ model: S3Model{}
38+ onCurrentRowChanged: {
39+ if (currentRow == -1) {
40+ root.result = null;
41+ return;
42+ }
43+ result = model.baseUrl + "/" + model.get(tableView.currentRow).key;
44+ }
45+ itemDelegate: Component {
46+ Item {
47+ clip: true
48+ Text {
49+ x: 3
50+ anchors.verticalCenter: parent.verticalCenter
51+ color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor
52+ elide: styleData.elideMode
53+ text: getText()
54+
55+ function getText() {
56+ switch(styleData.column) {
57+ case 1:
58+ return Utils.formatSize(styleData.value)
59+ default:
60+ return styleData.value;
61+ }
62+ }
63+
64+ }
65+ }
66+ }
67+ TableViewColumn {
68+ role: "name"
69+ title: "Name"
70+ width: 200
71+ }
72+ TableViewColumn {
73+ role: "size"
74+ title: "Size"
75+ width: 100
76+ }
77+ TableViewColumn {
78+ role: "modified"
79+ title: "Last Modified"
80+ width: 200
81+ }
82+ Rectangle {
83+ anchors.fill: parent
84+ visible: tableView.model.status !== XmlListModel.Ready
85+ color: "#7fffffff"
86+ BusyIndicator {
87+ anchors.centerIn: parent
88+ width: 48; height: 48
89+ running: true
90+ }
91+ }
92+ }
93+
94+ Row {
95+ id: buttonRow
96+ anchors { right: parent.right; bottom: parent.bottom }
97+ Button { action: acceptAction }
98+ Button { action: cancelAction }
99+ }
100+
101+ Action {
102+ id: acceptAction
103+ text: qsTr("OK")
104+ enabled: root.result ? true : false
105+ shortcut: Qt.Key_Return
106+ onTriggered: {
107+ root.selected(root.result);
108+ root.destroy();
109+ }
110+ }
111+
112+ Action {
113+ id: cancelAction
114+ text: qsTr("Cancel")
115+ shortcut: Qt.Key_Escape
116+ onTriggered: {
117+ root.canceled();
118+ root.destroy();
119+ }
120+ }
121+ }
122+
123+ }
124+}
125+
126+
127+
128+
interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml+128−0
@@ -0,0 +1,128 @@
1+import QtQuick 2.5
2+import QtQuick.Controls 1.4
3+import QtQuick.XmlListModel 2.0
4+
5+import "../../windows"
6+import "../../js/Utils.js" as Utils
7+import "../models"
8+
9+ModalWindow {
10+ id: root
11+ resizable: true
12+ width: 640
13+ height: 480
14+
15+ property var result;
16+
17+ signal selected(var modelUrl);
18+ signal canceled();
19+
20+ Rectangle {
21+ anchors.fill: parent
22+ color: "white"
23+
24+ Item {
25+ anchors { fill: parent; margins: 8 }
26+
27+ TextField {
28+ id: filterEdit
29+ anchors { left: parent.left; right: parent.right; top: parent.top }
30+ placeholderText: "filter"
31+ onTextChanged: tableView.model.filter = text
32+ }
33+
34+ TableView {
35+ id: tableView
36+ anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 }
37+ model: S3Model{}
38+ onCurrentRowChanged: {
39+ if (currentRow == -1) {
40+ root.result = null;
41+ return;
42+ }
43+ result = model.baseUrl + "/" + model.get(tableView.currentRow).key;
44+ }
45+ itemDelegate: Component {
46+ Item {
47+ clip: true
48+ Text {
49+ x: 3
50+ anchors.verticalCenter: parent.verticalCenter
51+ color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor
52+ elide: styleData.elideMode
53+ text: getText()
54+
55+ function getText() {
56+ switch(styleData.column) {
57+ case 1:
58+ return Utils.formatSize(styleData.value)
59+ default:
60+ return styleData.value;
61+ }
62+ }
63+
64+ }
65+ }
66+ }
67+ TableViewColumn {
68+ role: "name"
69+ title: "Name"
70+ width: 200
71+ }
72+ TableViewColumn {
73+ role: "size"
74+ title: "Size"
75+ width: 100
76+ }
77+ TableViewColumn {
78+ role: "modified"
79+ title: "Last Modified"
80+ width: 200
81+ }
82+ Rectangle {
83+ anchors.fill: parent
84+ visible: tableView.model.status !== XmlListModel.Ready
85+ color: "#7fffffff"
86+ BusyIndicator {
87+ anchors.centerIn: parent
88+ width: 48; height: 48
89+ running: true
90+ }
91+ }
92+ }
93+
94+ Row {
95+ id: buttonRow
96+ anchors { right: parent.right; bottom: parent.bottom }
97+ Button { action: acceptAction }
98+ Button { action: cancelAction }
99+ }
100+
101+ Action {
102+ id: acceptAction
103+ text: qsTr("OK")
104+ enabled: root.result ? true : false
105+ shortcut: Qt.Key_Return
106+ onTriggered: {
107+ root.selected(root.result);
108+ root.destroy();
109+ }
110+ }
111+
112+ Action {
113+ id: cancelAction
114+ text: qsTr("Cancel")
115+ shortcut: Qt.Key_Escape
116+ onTriggered: {
117+ root.canceled();
118+ root.destroy();
119+ }
120+ }
121+ }
122+
123+ }
124+}
125+
126+
127+
128+
interface/resources/qml/hifi/dialogs/attachments/Attachment.qml+164−0
@@ -0,0 +1,164 @@
1+import QtQuick 2.5
2+import QtQuick.Controls 1.4
3+
4+import "../../../windows"
5+import "."
6+import ".."
7+
8+Item {
9+ height: column.height + 2 * 8
10+
11+ property var attachment;
12+
13+ signal deleteAttachment(var attachment);
14+ signal updateAttachment();
15+ property bool completed: false;
16+
17+ Rectangle { color: "white"; anchors.fill: parent; radius: 4 }
18+
19+ Component.onCompleted: {
20+ completed = true;
21+ }
22+
23+ Column {
24+ y: 8
25+ id: column
26+ anchors { left: parent.left; right: parent.right; margins: 8 }
27+ spacing: 8
28+
29+ Item {
30+ height: modelChooserButton.height
31+ anchors { left: parent.left; right: parent.right; }
32+ Text { id: urlLabel; text: "Model URL:"; width: 80; anchors.verticalCenter: modelUrl.verticalCenter }
33+ TextField {
34+ id: modelUrl;
35+ height: jointChooser.height;
36+ anchors { left: urlLabel.right; leftMargin: 8; rightMargin: 8; right: modelChooserButton.left }
37+ text: attachment ? attachment.modelUrl : ""
38+ onTextChanged: {
39+ if (completed && attachment && attachment.modelUrl !== text) {
40+ attachment.modelUrl = text;
41+ updateAttachment();
42+ }
43+ }
44+ }
45+ Button {
46+ id: modelChooserButton;
47+ text: "Choose";
48+ anchors { right: parent.right; verticalCenter: modelUrl.verticalCenter }
49+ Component {
50+ id: modelBrowserBuiler;
51+ ModelBrowserDialog {}
52+ }
53+
54+ onClicked: {
55+ var browser = modelBrowserBuiler.createObject(desktop);
56+ browser.selected.connect(function(newModelUrl){
57+ modelUrl.text = newModelUrl;
58+ })
59+ }
60+ }
61+ }
62+
63+ Item {
64+ height: jointChooser.height
65+ anchors { left: parent.left; right: parent.right; }
66+ Text {
67+ id: jointLabel;
68+ width: 80;
69+ text: "Joint:";
70+ anchors.verticalCenter: jointChooser.verticalCenter;
71+ }
72+ ComboBox {
73+ id: jointChooser;
74+ anchors { left: jointLabel.right; leftMargin: 8; right: parent.right }
75+ model: MyAvatar.jointNames
76+ currentIndex: attachment ? model.indexOf(attachment.jointName) : -1
77+ onCurrentIndexChanged: {
78+ if (completed && attachment && currentIndex != -1 && currentText && currentText !== attachment.jointName) {
79+ attachment.jointName = currentText;
80+ updateAttachment();
81+ }
82+ }
83+ }
84+ }
85+
86+ Item {
87+ height: translation.height
88+ anchors { left: parent.left; right: parent.right; }
89+ Text { id: translationLabel; width: 80; text: "Translation:"; anchors.verticalCenter: translation.verticalCenter; }
90+ Translation {
91+ id: translation;
92+ anchors { left: translationLabel.right; leftMargin: 8; right: parent.right }
93+ vector: attachment ? attachment.translation : {x: 0, y: 0, z: 0};
94+ onValueChanged: {
95+ if (completed && attachment) {
96+ attachment.translation = vector;
97+ updateAttachment();
98+ }
99+ }
100+ }
101+ }
102+
103+ Item {
104+ height: rotation.height
105+ anchors { left: parent.left; right: parent.right; }
106+ Text { id: rotationLabel; width: 80; text: "Rotation:"; anchors.verticalCenter: rotation.verticalCenter; }
107+ Rotation {
108+ id: rotation;
109+ anchors { left: rotationLabel.right; leftMargin: 8; right: parent.right }
110+ vector: attachment ? attachment.rotation : {x: 0, y: 0, z: 0};
111+ onValueChanged: {
112+ if (completed && attachment) {
113+ attachment.rotation = vector;
114+ updateAttachment();
115+ }
116+ }
117+ }
118+ }
119+
120+ Item {
121+ height: scaleSpinner.height
122+ anchors { left: parent.left; right: parent.right; }
123+ Text { id: scaleLabel; width: 80; text: "Scale:"; anchors.verticalCenter: scale.verticalCenter; }
124+ SpinBox {
125+ id: scaleSpinner;
126+ anchors { left: scaleLabel.right; leftMargin: 8; right: parent.right }
127+ decimals: 1;
128+ minimumValue: 0.1
129+ maximumValue: 10
130+ stepSize: 0.1;
131+ value: attachment ? attachment.scale : 1.0
132+ onValueChanged: {
133+ if (completed && attachment && attachment.scale !== value) {
134+ attachment.scale = value;
135+ updateAttachment();
136+ }
137+ }
138+ }
139+ }
140+
141+ Item {
142+ height: soft.height
143+ anchors { left: parent.left; right: parent.right; }
144+ Text { id: softLabel; width: 80; text: "Is soft:"; anchors.verticalCenter: soft.verticalCenter; }
145+ CheckBox {
146+ id: soft;
147+ anchors { left: softLabel.right; leftMargin: 8; right: parent.right }
148+ checked: attachment ? attachment.soft : false
149+ onCheckedChanged: {
150+ if (completed && attachment && attachment.soft !== checked) {
151+ attachment.soft = checked;
152+ updateAttachment();
153+ }
154+ }
155+ }
156+ }
157+
158+ Button {
159+ anchors { left: parent.left; right: parent.right; }
160+ text: "Delete"
161+ onClicked: deleteAttachment(root.attachment);
162+ }
163+ }
164+}
interface/resources/qml/hifi/dialogs/attachments/Rotation.qml+9−0
@@ -0,0 +1,9 @@
1+import "."
2+
3+Vector3 {
4+ decimals: 1;
5+ stepSize: 1;
6+ maximumValue: 180
7+ minimumValue: -180
8+}
9+
interface/resources/qml/hifi/dialogs/attachments/Translation.qml+9−0
@@ -0,0 +1,9 @@
1+import "."
2+
3+Vector3 {
4+ decimals: 2;
5+ stepSize: 0.01;
6+ maximumValue: 10
7+ minimumValue: -10
8+}
9+
interface/resources/qml/hifi/dialogs/attachments/Vector3.qml+71−0
@@ -0,0 +1,71 @@
1+import QtQuick 2.5
2+import QtQuick.Controls 1.4
3+
4+Item {
5+ id: root
6+ implicitHeight: xspinner.height
7+ readonly property real spacing: 8
8+ property real spinboxWidth: (width / 3) - spacing
9+ property var vector;
10+ property real decimals: 0
11+ property real stepSize: 1
12+ property real maximumValue: 99
13+ property real minimumValue: 0
14+
15+ signal valueChanged();
16+
17+ SpinBox {
18+ id: xspinner
19+ width: root.spinboxWidth
20+ anchors { left: parent.left }
21+ value: root.vector.x
22+
23+ decimals: root.decimals
24+ stepSize: root.stepSize
25+ maximumValue: root.maximumValue
26+ minimumValue: root.minimumValue
27+ onValueChanged: {
28+ if (value !== vector.x) {
29+ vector.x = value
30+ root.valueChanged();
31+ }
32+ }
33+ }
34+
35+ SpinBox {
36+ id: yspinner
37+ width: root.spinboxWidth
38+ anchors { horizontalCenter: parent.horizontalCenter }
39+ value: root.vector.y
40+
41+ decimals: root.decimals
42+ stepSize: root.stepSize
43+ maximumValue: root.maximumValue
44+ minimumValue: root.minimumValue
45+ onValueChanged: {
46+ if (value !== vector.y) {
47+ vector.y = value
48+ root.valueChanged();
49+ }
50+ }
51+ }
52+
53+ SpinBox {
54+ id: zspinner
55+ width: root.spinboxWidth
56+ anchors { right: parent.right; }
57+ value: root.vector.z
58+
59+ decimals: root.decimals
60+ stepSize: root.stepSize
61+ maximumValue: root.maximumValue
62+ minimumValue: root.minimumValue
63+ onValueChanged: {
64+ if (value !== vector.z) {
65+ vector.z = value
66+ root.valueChanged();
67+ }
68+ }
69+ }
70+}
71+
interface/resources/qml/hifi/models/S3Model.qml+41−0
@@ -0,0 +1,41 @@
1+import QtQuick 2.0
2+import QtQuick.XmlListModel 2.0
3+
4+//http://s3.amazonaws.com/hifi-public?prefix=models/attachments
5+/*
6+<?xml version="1.0" encoding="UTF-8"?>
7+<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
8+ <Contents>
9+ <Key>models/attachments/guitar.fst</Key>
10+ <LastModified>2015-11-10T00:28:22.000Z</LastModified>
11+ <ETag>&quot;236c00c4802ba9c2605cabd5601d138e&quot;</ETag>
12+ <Size>2992</Size>
13+ <StorageClass>STANDARD</StorageClass>
14+ </Contents>
15+</ListBucketResult>
16+*/
17+
18+// FIXME how to deal with truncated results? Store the marker?
19+XmlListModel {
20+ id: xmlModel
21+ property string prefix: "models/attachments/"
22+ property string extension: "fst"
23+ property string filter;
24+
25+ readonly property string realPrefix: prefix.match('.*/$') ? prefix : (prefix + "/")
26+ readonly property string nameRegex: realPrefix + (filter ? (".*" + filter) : "") + ".*\." + extension
27+ readonly property string nameQuery: "Key/substring-before(substring-after(string(), '" + prefix + "'), '." + extension + "')"
28+ readonly property string baseUrl: "http://s3.amazonaws.com/hifi-public"
29+
30+ // FIXME need to urlencode prefix?
31+ source: baseUrl + "?prefix=" + realPrefix
32+ query: "/ListBucketResult/Contents[matches(Key, '" + nameRegex + "')]"
33+ namespaceDeclarations: "declare default element namespace 'http://s3.amazonaws.com/doc/2006-03-01/';"
34+
35+ XmlRole { name: "name"; query: nameQuery }
36+ XmlRole { name: "size"; query: "Size/string()" }
37+ XmlRole { name: "tag"; query: "ETag/string()" }
38+ XmlRole { name: "modified"; query: "LastModified/string()" }
39+ XmlRole { name: "key"; query: "Key/string()" }
40+}
41+
interface/resources/qml/js/Utils.js+14−0
@@ -14,3 +14,17 @@ function randomPosition(min, max) { · 13 unmodified lines
1414 Math.random() * (max.x - min.x),
1515 Math.random() * (max.y - min.y));
1616 }
17+
18+function formatSize(size) {
19+ var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ];
20+ var suffixIndex = 0
21+ while ((size / 1024.0) > 1.1) {
22+ size /= 1024.0;
23+ ++suffixIndex;
24+ }
25+
26+ size = Math.round(size*1000)/1000;
27+ size = size.toLocaleString()
28+
29+ return size + " " + suffixes[suffixIndex];
30+}
interface/src/Application.cpp+5−0
@@ -1186,6 +1186,7 @@ void Application::initializeUi() { · 1185 unmodified lines
11861186 UpdateDialog::registerType();
11871187 qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
11881188
1189+
11891190 auto offscreenUi = DependencyManager::get<OffscreenUi>();
11901191 offscreenUi->create(_offscreenContext->getContext());
11911192 offscreenUi->setProxyWindow(_window->windowHandle());
@@ -1842,6 +1843,10 @@ void Application::keyPressEvent(QKeyEvent* event) { · 650 unmodified lines
18421843
18431844 case Qt::Key_X:
18441845 if (isShifted && isMeta) {
1846+ auto offscreenUi = DependencyManager::get<OffscreenUi>();
1847+ offscreenUi->getRootContext()->engine()->clearComponentCache();
1848+ offscreenUi->load("hifi/dialogs/AttachmentsDialog.qml");
1849+ // OffscreenUi::information("Debugging", "Component cache cleared");
18451850 // placeholder for dialogs being converted to QML.
18461851 }
18471852 break;
libraries/avatars/src/AvatarData.cpp+63−0
@@ -25,6 +25,7 @@ · 24 unmodified lines
2525 #include <QtNetwork/QNetworkReply>
2626 #include <QtNetwork/QNetworkRequest>
2727
28+#include <QVariantGLM.h>
2829 #include <Transform.h>
2930 #include <NetworkAccessManager.h>
3031 #include <NodeList.h>
@@ -1673,3 +1674,65 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const · 1642 unmodified lines
16731674 assert(false);
16741675 return glm::vec3();
16751676 }
1677+
1678+QVariant AttachmentData::toVariant() const {
1679+ QVariantMap result;
1680+ result["modelUrl"] = modelURL;
1681+ result["jointName"] = jointName;
1682+ result["translation"] = glmToQMap(translation);
1683+ result["rotation"] = glmToQMap(glm::degrees(safeEulerAngles(rotation)));
1684+ result["scale"] = scale;
1685+ result["soft"] = isSoft;
1686+ return result;
1687+}
1688+
1689+glm::vec3 variantToVec3(const QVariant& var) {
1690+ auto map = var.toMap();
1691+ glm::vec3 result;
1692+ result.x = map["x"].toFloat();
1693+ result.y = map["y"].toFloat();
1694+ result.z = map["z"].toFloat();
1695+ return result;
1696+}
1697+
1698+void AttachmentData::fromVariant(const QVariant& variant) {
1699+ auto map = variant.toMap();
1700+ if (map.contains("modelUrl")) {
1701+ auto urlString = map["modelUrl"].toString();
1702+ modelURL = urlString;
1703+ }
1704+ if (map.contains("jointName")) {
1705+ jointName = map["jointName"].toString();
1706+ }
1707+ if (map.contains("translation")) {
1708+ translation = variantToVec3(map["translation"]);
1709+ }
1710+ if (map.contains("rotation")) {
1711+ rotation = glm::quat(glm::radians(variantToVec3(map["rotation"])));
1712+ }
1713+ if (map.contains("scale")) {
1714+ scale = map["scale"].toFloat();
1715+ }
1716+ if (map.contains("soft")) {
1717+ isSoft = map["soft"].toBool();
1718+ }
1719+}
1720+
1721+QVariantList AvatarData::getAttachmentsVariant() const {
1722+ QVariantList result;
1723+ for (const auto& attachment : getAttachmentData()) {
1724+ result.append(attachment.toVariant());
1725+ }
1726+ return result;
1727+}
1728+
1729+void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
1730+ QVector<AttachmentData> newAttachments;
1731+ newAttachments.reserve(variant.size());
1732+ for (const auto& attachmentVar : variant) {
1733+ AttachmentData attachment;
1734+ attachment.fromVariant(attachmentVar);
1735+ newAttachments.append(attachment);
1736+ }
1737+ setAttachmentData(newAttachments);
1738+}
libraries/avatars/src/AvatarData.h+6−0
@@ -277,6 +277,9 @@ public: · 276 unmodified lines
277277
278278 Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); }
279279
280+ Q_INVOKABLE QVariantList getAttachmentsVariant() const;
281+ Q_INVOKABLE void setAttachmentsVariant(const QVariantList& variant);
282+
280283 void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; }
281284
282285 // key state
@@ -448,6 +451,9 @@ public: · 165 unmodified lines
448451
449452 QJsonObject toJson() const;
450453 void fromJson(const QJsonObject& json);
454+
455+ QVariant toVariant() const;
456+ void fromVariant(const QVariant& variant);
451457 };
452458
453459 QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment);
libraries/networking/src/LimitedNodeList.cpp+1−1
@@ -959,7 +959,7 @@ void LimitedNodeList::putLocalPortIntoSharedMemory(const QString key, QObject* p · 958 unmodified lines
959959 bool LimitedNodeList::getLocalServerPortFromSharedMemory(const QString key, quint16& localPort) {
960960 QSharedMemory sharedMem(key);
961961 if (!sharedMem.attach(QSharedMemory::ReadOnly)) {
962− qWarning() << "Could not attach to shared memory at key" << key;
962+ qCWarning(networking) << "Could not attach to shared memory at key" << key;
963963 return false;
964964 } else {
965965 sharedMem.lock();
tests/ui/qml/main.qml+12−0
@@ -7,6 +7,7 @@ import "../../../interface/resources/qml" · 6 unmodified lines
77 import "../../../interface/resources/qml/windows"
88 import "../../../interface/resources/qml/dialogs"
99 import "../../../interface/resources/qml/hifi"
10+import "../../../interface/resources/qml/hifi/dialogs"
1011
1112 ApplicationWindow {
1213 id: appWindow
@@ -196,4 +197,15 @@ ApplicationWindow { · 183 unmodified lines
196197 }
197198 */
198199 }
200+
201+ Action {
202+ text: "Open Browser"
203+ shortcut: "Ctrl+Shift+X"
204+ onTriggered: {
205+ builder.createObject(desktop);
206+ }
207+ property var builder: Component {
208+ ModelBrowserDialog{}
209+ }
210+ }
199211 }