From 664100b9b14e13eeea2ff2aae71be21c95dbd571 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sat, 23 Jan 2016 17:14:30 -0800 Subject: [PATCH 1/3] Attachment dialog --- .../qml/hifi/dialogs/AttachmentsDialog.qml | 123 +++++++++++++ .../hifi/dialogs/AvatarAttachmentsDialog.qml | 128 ++++++++++++++ .../qml/hifi/dialogs/ModelBrowserDialog.qml | 128 ++++++++++++++ .../hifi/dialogs/attachments/Attachment.qml | 164 ++++++++++++++++++ .../qml/hifi/dialogs/attachments/Rotation.qml | 9 + .../hifi/dialogs/attachments/Translation.qml | 9 + .../qml/hifi/dialogs/attachments/Vector3.qml | 71 ++++++++ .../resources/qml/hifi/models/S3Model.qml | 41 +++++ interface/resources/qml/js/Utils.js | 14 ++ interface/src/Application.cpp | 5 + libraries/avatars/src/AvatarData.cpp | 63 +++++++ libraries/avatars/src/AvatarData.h | 6 + libraries/networking/src/LimitedNodeList.cpp | 2 +- tests/ui/qml/main.qml | 12 ++ 14 files changed, 774 insertions(+), 1 deletion(-) create mode 100644 interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml create mode 100644 interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml create mode 100644 interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml create mode 100644 interface/resources/qml/hifi/dialogs/attachments/Attachment.qml create mode 100644 interface/resources/qml/hifi/dialogs/attachments/Rotation.qml create mode 100644 interface/resources/qml/hifi/dialogs/attachments/Translation.qml create mode 100644 interface/resources/qml/hifi/dialogs/attachments/Vector3.qml create mode 100644 interface/resources/qml/hifi/models/S3Model.qml diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml new file mode 100644 index 0000000000..77771f65c4 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -0,0 +1,123 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../windows" +import "attachments" + +Window { + id: root + title: "Edit Attachments" + width: 600 + height: 600 + resizable: true + // User must click OK or cancel to close the window + closable: false + + readonly property var originalAttachments: MyAvatar.getAttachmentsVariant(); + property var attachments: []; + + property var settings: Settings { + category: "AttachmentsDialog" + property alias x: root.x + property alias y: root.y + property alias width: root.width + property alias height: root.height + } + + Component.onCompleted: { + for (var i = 0; i < originalAttachments.length; ++i) { + var attachment = originalAttachments[i]; + root.attachments.push(attachment); + listView.model.append({}); + } + } + + Rectangle { + anchors.fill: parent + radius: 4 + + Rectangle { + id: attachmentsBackground + anchors { left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; margins: 8 } + color: "gray" + radius: 4 + + ScrollView{ + id: scrollView + anchors.fill: parent + anchors.margins: 4 + ListView { + id: listView + model: ListModel {} + delegate: Item { + implicitHeight: attachmentView.height + 8; + implicitWidth: attachmentView.width; + Attachment { + id: attachmentView + width: scrollView.width + attachment: root.attachments[index] + onDeleteAttachment: { + attachments.splice(index, 1); + listView.model.remove(index, 1); + } + onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); + } + } + onCountChanged: MyAvatar.setAttachmentsVariant(attachments); + } + } + } + + Button { + id: newAttachmentButton + anchors { left: parent.left; right: parent.right; bottom: buttonRow.top; margins: 8 } + text: "New Attachment" + + onClicked: { + var template = { + modelUrl: "", + translation: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: 1, + jointName: MyAvatar.jointNames[0], + soft: false + }; + attachments.push(template); + listView.model.append({}); + MyAvatar.setAttachmentsVariant(attachments); + } + } + + Row { + id: buttonRow + spacing: 8 + anchors { right: parent.right; bottom: parent.bottom; margins: 8 } + Button { action: cancelAction } + Button { action: okAction } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { + MyAvatar.setAttachmentsVariant(originalAttachments); + root.destroy() + } + } + + Action { + id: okAction + text: "OK" + onTriggered: { + for (var i = 0; i < attachments.length; ++i) { + console.log("Attachment " + i + ": " + attachments[i]); + } + + MyAvatar.setAttachmentsVariant(attachments); + root.destroy() + } + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml new file mode 100644 index 0000000000..252e4c629e --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/AvatarAttachmentsDialog.qml @@ -0,0 +1,128 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.XmlListModel 2.0 + +import "../../windows" +import "../../js/Utils.js" as Utils +import "../models" + +ModalWindow { + id: root + resizable: true + width: 640 + height: 480 + + property var result; + + signal selected(var modelUrl); + signal canceled(); + + Rectangle { + anchors.fill: parent + color: "white" + + Item { + anchors { fill: parent; margins: 8 } + + TextField { + id: filterEdit + anchors { left: parent.left; right: parent.right; top: parent.top } + placeholderText: "filter" + onTextChanged: tableView.model.filter = text + } + + TableView { + id: tableView + anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 } + model: S3Model{} + onCurrentRowChanged: { + if (currentRow == -1) { + root.result = null; + return; + } + result = model.baseUrl + "/" + model.get(tableView.currentRow).key; + } + itemDelegate: Component { + Item { + clip: true + Text { + x: 3 + anchors.verticalCenter: parent.verticalCenter + color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor + elide: styleData.elideMode + text: getText() + + function getText() { + switch(styleData.column) { + case 1: + return Utils.formatSize(styleData.value) + default: + return styleData.value; + } + } + + } + } + } + TableViewColumn { + role: "name" + title: "Name" + width: 200 + } + TableViewColumn { + role: "size" + title: "Size" + width: 100 + } + TableViewColumn { + role: "modified" + title: "Last Modified" + width: 200 + } + Rectangle { + anchors.fill: parent + visible: tableView.model.status !== XmlListModel.Ready + color: "#7fffffff" + BusyIndicator { + anchors.centerIn: parent + width: 48; height: 48 + running: true + } + } + } + + Row { + id: buttonRow + anchors { right: parent.right; bottom: parent.bottom } + Button { action: acceptAction } + Button { action: cancelAction } + } + + Action { + id: acceptAction + text: qsTr("OK") + enabled: root.result ? true : false + shortcut: Qt.Key_Return + onTriggered: { + root.selected(root.result); + root.destroy(); + } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + } + + } +} + + + + diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml new file mode 100644 index 0000000000..252e4c629e --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -0,0 +1,128 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.XmlListModel 2.0 + +import "../../windows" +import "../../js/Utils.js" as Utils +import "../models" + +ModalWindow { + id: root + resizable: true + width: 640 + height: 480 + + property var result; + + signal selected(var modelUrl); + signal canceled(); + + Rectangle { + anchors.fill: parent + color: "white" + + Item { + anchors { fill: parent; margins: 8 } + + TextField { + id: filterEdit + anchors { left: parent.left; right: parent.right; top: parent.top } + placeholderText: "filter" + onTextChanged: tableView.model.filter = text + } + + TableView { + id: tableView + anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 } + model: S3Model{} + onCurrentRowChanged: { + if (currentRow == -1) { + root.result = null; + return; + } + result = model.baseUrl + "/" + model.get(tableView.currentRow).key; + } + itemDelegate: Component { + Item { + clip: true + Text { + x: 3 + anchors.verticalCenter: parent.verticalCenter + color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor + elide: styleData.elideMode + text: getText() + + function getText() { + switch(styleData.column) { + case 1: + return Utils.formatSize(styleData.value) + default: + return styleData.value; + } + } + + } + } + } + TableViewColumn { + role: "name" + title: "Name" + width: 200 + } + TableViewColumn { + role: "size" + title: "Size" + width: 100 + } + TableViewColumn { + role: "modified" + title: "Last Modified" + width: 200 + } + Rectangle { + anchors.fill: parent + visible: tableView.model.status !== XmlListModel.Ready + color: "#7fffffff" + BusyIndicator { + anchors.centerIn: parent + width: 48; height: 48 + running: true + } + } + } + + Row { + id: buttonRow + anchors { right: parent.right; bottom: parent.bottom } + Button { action: acceptAction } + Button { action: cancelAction } + } + + Action { + id: acceptAction + text: qsTr("OK") + enabled: root.result ? true : false + shortcut: Qt.Key_Return + onTriggered: { + root.selected(root.result); + root.destroy(); + } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + } + + } +} + + + + diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml new file mode 100644 index 0000000000..b45e10d755 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -0,0 +1,164 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../../windows" +import "." +import ".." + +Item { + height: column.height + 2 * 8 + + property var attachment; + + signal deleteAttachment(var attachment); + signal updateAttachment(); + property bool completed: false; + + Rectangle { color: "white"; anchors.fill: parent; radius: 4 } + + Component.onCompleted: { + completed = true; + } + + Column { + y: 8 + id: column + anchors { left: parent.left; right: parent.right; margins: 8 } + spacing: 8 + + Item { + height: modelChooserButton.height + anchors { left: parent.left; right: parent.right; } + Text { id: urlLabel; text: "Model URL:"; width: 80; anchors.verticalCenter: modelUrl.verticalCenter } + TextField { + id: modelUrl; + height: jointChooser.height; + anchors { left: urlLabel.right; leftMargin: 8; rightMargin: 8; right: modelChooserButton.left } + text: attachment ? attachment.modelUrl : "" + onTextChanged: { + if (completed && attachment && attachment.modelUrl !== text) { + attachment.modelUrl = text; + updateAttachment(); + } + } + } + Button { + id: modelChooserButton; + text: "Choose"; + anchors { right: parent.right; verticalCenter: modelUrl.verticalCenter } + Component { + id: modelBrowserBuiler; + ModelBrowserDialog {} + } + + onClicked: { + var browser = modelBrowserBuiler.createObject(desktop); + browser.selected.connect(function(newModelUrl){ + modelUrl.text = newModelUrl; + }) + } + } + } + + Item { + height: jointChooser.height + anchors { left: parent.left; right: parent.right; } + Text { + id: jointLabel; + width: 80; + text: "Joint:"; + anchors.verticalCenter: jointChooser.verticalCenter; + } + ComboBox { + id: jointChooser; + anchors { left: jointLabel.right; leftMargin: 8; right: parent.right } + model: MyAvatar.jointNames + currentIndex: attachment ? model.indexOf(attachment.jointName) : -1 + onCurrentIndexChanged: { + if (completed && attachment && currentIndex != -1 && currentText && currentText !== attachment.jointName) { + attachment.jointName = currentText; + updateAttachment(); + } + } + } + } + + Item { + height: translation.height + anchors { left: parent.left; right: parent.right; } + Text { id: translationLabel; width: 80; text: "Translation:"; anchors.verticalCenter: translation.verticalCenter; } + Translation { + id: translation; + anchors { left: translationLabel.right; leftMargin: 8; right: parent.right } + vector: attachment ? attachment.translation : {x: 0, y: 0, z: 0}; + onValueChanged: { + if (completed && attachment) { + attachment.translation = vector; + updateAttachment(); + } + } + } + } + + Item { + height: rotation.height + anchors { left: parent.left; right: parent.right; } + Text { id: rotationLabel; width: 80; text: "Rotation:"; anchors.verticalCenter: rotation.verticalCenter; } + Rotation { + id: rotation; + anchors { left: rotationLabel.right; leftMargin: 8; right: parent.right } + vector: attachment ? attachment.rotation : {x: 0, y: 0, z: 0}; + onValueChanged: { + if (completed && attachment) { + attachment.rotation = vector; + updateAttachment(); + } + } + } + } + + Item { + height: scaleSpinner.height + anchors { left: parent.left; right: parent.right; } + Text { id: scaleLabel; width: 80; text: "Scale:"; anchors.verticalCenter: scale.verticalCenter; } + SpinBox { + id: scaleSpinner; + anchors { left: scaleLabel.right; leftMargin: 8; right: parent.right } + decimals: 1; + minimumValue: 0.1 + maximumValue: 10 + stepSize: 0.1; + value: attachment ? attachment.scale : 1.0 + onValueChanged: { + if (completed && attachment && attachment.scale !== value) { + attachment.scale = value; + updateAttachment(); + } + } + } + } + + Item { + height: soft.height + anchors { left: parent.left; right: parent.right; } + Text { id: softLabel; width: 80; text: "Is soft:"; anchors.verticalCenter: soft.verticalCenter; } + CheckBox { + id: soft; + anchors { left: softLabel.right; leftMargin: 8; right: parent.right } + checked: attachment ? attachment.soft : false + onCheckedChanged: { + if (completed && attachment && attachment.soft !== checked) { + attachment.soft = checked; + updateAttachment(); + } + } + } + } + + Button { + anchors { left: parent.left; right: parent.right; } + text: "Delete" + onClicked: deleteAttachment(root.attachment); + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/attachments/Rotation.qml b/interface/resources/qml/hifi/dialogs/attachments/Rotation.qml new file mode 100644 index 0000000000..6061efc4c8 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/attachments/Rotation.qml @@ -0,0 +1,9 @@ +import "." + +Vector3 { + decimals: 1; + stepSize: 1; + maximumValue: 180 + minimumValue: -180 +} + diff --git a/interface/resources/qml/hifi/dialogs/attachments/Translation.qml b/interface/resources/qml/hifi/dialogs/attachments/Translation.qml new file mode 100644 index 0000000000..f3a90cdf94 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/attachments/Translation.qml @@ -0,0 +1,9 @@ +import "." + +Vector3 { + decimals: 2; + stepSize: 0.01; + maximumValue: 10 + minimumValue: -10 +} + diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml new file mode 100644 index 0000000000..02382749bd --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -0,0 +1,71 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + id: root + implicitHeight: xspinner.height + readonly property real spacing: 8 + property real spinboxWidth: (width / 3) - spacing + property var vector; + property real decimals: 0 + property real stepSize: 1 + property real maximumValue: 99 + property real minimumValue: 0 + + signal valueChanged(); + + SpinBox { + id: xspinner + width: root.spinboxWidth + anchors { left: parent.left } + value: root.vector.x + + decimals: root.decimals + stepSize: root.stepSize + maximumValue: root.maximumValue + minimumValue: root.minimumValue + onValueChanged: { + if (value !== vector.x) { + vector.x = value + root.valueChanged(); + } + } + } + + SpinBox { + id: yspinner + width: root.spinboxWidth + anchors { horizontalCenter: parent.horizontalCenter } + value: root.vector.y + + decimals: root.decimals + stepSize: root.stepSize + maximumValue: root.maximumValue + minimumValue: root.minimumValue + onValueChanged: { + if (value !== vector.y) { + vector.y = value + root.valueChanged(); + } + } + } + + SpinBox { + id: zspinner + width: root.spinboxWidth + anchors { right: parent.right; } + value: root.vector.z + + decimals: root.decimals + stepSize: root.stepSize + maximumValue: root.maximumValue + minimumValue: root.minimumValue + onValueChanged: { + if (value !== vector.z) { + vector.z = value + root.valueChanged(); + } + } + } +} + diff --git a/interface/resources/qml/hifi/models/S3Model.qml b/interface/resources/qml/hifi/models/S3Model.qml new file mode 100644 index 0000000000..565965c124 --- /dev/null +++ b/interface/resources/qml/hifi/models/S3Model.qml @@ -0,0 +1,41 @@ +import QtQuick 2.0 +import QtQuick.XmlListModel 2.0 + +//http://s3.amazonaws.com/hifi-public?prefix=models/attachments +/* + + + + models/attachments/guitar.fst + 2015-11-10T00:28:22.000Z + "236c00c4802ba9c2605cabd5601d138e" + 2992 + STANDARD + + +*/ + +// FIXME how to deal with truncated results? Store the marker? +XmlListModel { + id: xmlModel + property string prefix: "models/attachments/" + property string extension: "fst" + property string filter; + + readonly property string realPrefix: prefix.match('.*/$') ? prefix : (prefix + "/") + readonly property string nameRegex: realPrefix + (filter ? (".*" + filter) : "") + ".*\." + extension + readonly property string nameQuery: "Key/substring-before(substring-after(string(), '" + prefix + "'), '." + extension + "')" + readonly property string baseUrl: "http://s3.amazonaws.com/hifi-public" + + // FIXME need to urlencode prefix? + source: baseUrl + "?prefix=" + realPrefix + query: "/ListBucketResult/Contents[matches(Key, '" + nameRegex + "')]" + namespaceDeclarations: "declare default element namespace 'http://s3.amazonaws.com/doc/2006-03-01/';" + + XmlRole { name: "name"; query: nameQuery } + XmlRole { name: "size"; query: "Size/string()" } + XmlRole { name: "tag"; query: "ETag/string()" } + XmlRole { name: "modified"; query: "LastModified/string()" } + XmlRole { name: "key"; query: "Key/string()" } +} + diff --git a/interface/resources/qml/js/Utils.js b/interface/resources/qml/js/Utils.js index 417c8c1641..49f85fcc91 100644 --- a/interface/resources/qml/js/Utils.js +++ b/interface/resources/qml/js/Utils.js @@ -14,3 +14,17 @@ function randomPosition(min, max) { Math.random() * (max.x - min.x), Math.random() * (max.y - min.y)); } + +function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4629a8c08c..b3142702bb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1186,6 +1186,7 @@ void Application::initializeUi() { UpdateDialog::registerType(); qmlRegisterType("Hifi", 1, 0, "Preference"); + auto offscreenUi = DependencyManager::get(); offscreenUi->create(_offscreenContext->getContext()); offscreenUi->setProxyWindow(_window->windowHandle()); @@ -1842,6 +1843,10 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_X: if (isShifted && isMeta) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->getRootContext()->engine()->clearComponentCache(); + offscreenUi->load("hifi/dialogs/AttachmentsDialog.qml"); + // OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } break; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 5fd69128eb..3933d705fc 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -1673,3 +1674,65 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const assert(false); return glm::vec3(); } + +QVariant AttachmentData::toVariant() const { + QVariantMap result; + result["modelUrl"] = modelURL; + result["jointName"] = jointName; + result["translation"] = glmToQMap(translation); + result["rotation"] = glmToQMap(glm::degrees(safeEulerAngles(rotation))); + result["scale"] = scale; + result["soft"] = isSoft; + return result; +} + +glm::vec3 variantToVec3(const QVariant& var) { + auto map = var.toMap(); + glm::vec3 result; + result.x = map["x"].toFloat(); + result.y = map["y"].toFloat(); + result.z = map["z"].toFloat(); + return result; +} + +void AttachmentData::fromVariant(const QVariant& variant) { + auto map = variant.toMap(); + if (map.contains("modelUrl")) { + auto urlString = map["modelUrl"].toString(); + modelURL = urlString; + } + if (map.contains("jointName")) { + jointName = map["jointName"].toString(); + } + if (map.contains("translation")) { + translation = variantToVec3(map["translation"]); + } + if (map.contains("rotation")) { + rotation = glm::quat(glm::radians(variantToVec3(map["rotation"]))); + } + if (map.contains("scale")) { + scale = map["scale"].toFloat(); + } + if (map.contains("soft")) { + isSoft = map["soft"].toBool(); + } +} + +QVariantList AvatarData::getAttachmentsVariant() const { + QVariantList result; + for (const auto& attachment : getAttachmentData()) { + result.append(attachment.toVariant()); + } + return result; +} + +void AvatarData::setAttachmentsVariant(const QVariantList& variant) { + QVector newAttachments; + newAttachments.reserve(variant.size()); + for (const auto& attachmentVar : variant) { + AttachmentData attachment; + attachment.fromVariant(attachmentVar); + newAttachments.append(attachment); + } + setAttachmentData(newAttachments); +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 3b8214f226..eac1917533 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -277,6 +277,9 @@ public: Q_INVOKABLE void setBlendshape(QString name, float val) { _headData->setBlendshape(name, val); } + Q_INVOKABLE QVariantList getAttachmentsVariant() const; + Q_INVOKABLE void setAttachmentsVariant(const QVariantList& variant); + void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } // key state @@ -448,6 +451,9 @@ public: QJsonObject toJson() const; void fromJson(const QJsonObject& json); + + QVariant toVariant() const; + void fromVariant(const QVariant& variant); }; QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 485100da0a..3ea3175390 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -959,7 +959,7 @@ void LimitedNodeList::putLocalPortIntoSharedMemory(const QString key, QObject* p bool LimitedNodeList::getLocalServerPortFromSharedMemory(const QString key, quint16& localPort) { QSharedMemory sharedMem(key); if (!sharedMem.attach(QSharedMemory::ReadOnly)) { - qWarning() << "Could not attach to shared memory at key" << key; + qCWarning(networking) << "Could not attach to shared memory at key" << key; return false; } else { sharedMem.lock(); diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index c970cd711b..d20b580b5a 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -7,6 +7,7 @@ import "../../../interface/resources/qml" import "../../../interface/resources/qml/windows" import "../../../interface/resources/qml/dialogs" import "../../../interface/resources/qml/hifi" +import "../../../interface/resources/qml/hifi/dialogs" ApplicationWindow { id: appWindow @@ -196,4 +197,15 @@ ApplicationWindow { } */ } + + Action { + text: "Open Browser" + shortcut: "Ctrl+Shift+X" + onTriggered: { + builder.createObject(desktop); + } + property var builder: Component { + ModelBrowserDialog{} + } + } } From 2956557c5f50f8e5dd9b81ee4ed42629a54b8892 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 26 Jan 2016 14:41:22 -0800 Subject: [PATCH 2/3] Adding overlay compatible combo-box --- interface/resources/qml/controls/ComboBox.qml | 148 ++++++++++++++++++ .../hifi/dialogs/attachments/Attachment.qml | 3 +- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 interface/resources/qml/controls/ComboBox.qml diff --git a/interface/resources/qml/controls/ComboBox.qml b/interface/resources/qml/controls/ComboBox.qml new file mode 100644 index 0000000000..e22bc8e664 --- /dev/null +++ b/interface/resources/qml/controls/ComboBox.qml @@ -0,0 +1,148 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import "." as VrControls + +FocusScope { + id: root + property alias model: comboBox.model; + readonly property alias currentText: comboBox.currentText; + property alias currentIndex: comboBox.currentIndex; + implicitHeight: comboBox.height; + focus: true + + readonly property ComboBox control: comboBox + + Rectangle { + id: background + gradient: Gradient { + GradientStop {color: control.pressed ? "#bababa" : "#fefefe" ; position: 0} + GradientStop {color: control.pressed ? "#ccc" : "#e3e3e3" ; position: 1} + } + anchors.fill: parent + border.color: control.activeFocus ? "#47b" : "#999" + Rectangle { + anchors.fill: parent + radius: parent.radius + color: control.activeFocus ? "#47b" : "white" + opacity: control.hovered || control.activeFocus ? 0.1 : 0 + Behavior on opacity {NumberAnimation{ duration: 100 }} + } + } + + SystemPalette { id: palette } + + ComboBox { + id: comboBox + anchors.fill: parent + visible: false + } + + Text { + id: textField + anchors { left: parent.left; leftMargin: 2; right: dropIcon.left; verticalCenter: parent.verticalCenter } + text: comboBox.currentText + elide: Text.ElideRight + } + + Item { + id: dropIcon + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + width: 20 + height: textField.height + VrControls.FontAwesome { + anchors.centerIn: parent; size: 16; + text: "\uf0d7" + } + } + + MouseArea { + anchors.fill: parent + onClicked: toggleList(); + } + + function toggleList() { + if (popup.visible) { + hideList(); + } else { + showList(); + } + } + + function showList() { + var r = desktop.mapFromItem(root, 0, 0, root.width, root.height); + listView.currentIndex = root.currentIndex + scrollView.x = r.x; + scrollView.y = r.y + r.height; + var bottom = scrollView.y + scrollView.height; + if (bottom > desktop.height) { + scrollView.y -= bottom - desktop.height + 8; + } + popup.visible = true; + popup.forceActiveFocus(); + } + + function hideList() { + popup.visible = false; + } + + FocusScope { + id: popup + parent: desktop + anchors.fill: parent + visible: false + focus: true + MouseArea { + anchors.fill: parent + onClicked: hideList(); + } + + function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } + function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } + function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); } + + Keys.onUpPressed: previousItem(); + Keys.onDownPressed: nextItem(); + Keys.onSpacePressed: selectCurrentItem(); + Keys.onRightPressed: selectCurrentItem(); + Keys.onReturnPressed: selectCurrentItem(); + Keys.onEscapePressed: hideList(); + + ScrollView { + id: scrollView + height: 480 + width: root.width + + ListView { + id: listView + height: textView.height * count + model: root.model + highlight: Rectangle{ + width: listView.currentItem ? listView.currentItem.width : 0 + height: listView.currentItem ? listView.currentItem.height : 0 + color: "red" + } + delegate: Rectangle { + width: root.width + height: popupText.implicitHeight * 1.4 + color: ListView.isCurrentItem ? palette.highlight : palette.base + Text { + anchors.verticalCenter: parent.verticalCenter + id: popupText + x: 3 + text: listView.model[index] + } + MouseArea { + id: popupHover + anchors.fill: parent; + hoverEnabled: true + onEntered: listView.currentIndex = index; + onClicked: popup.selectCurrentItem() + } + } + } + } + } + +} diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index b45e10d755..31a1895e58 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -2,6 +2,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import "../../../windows" +import "../../../controls" as VrControls import "." import ".." @@ -69,7 +70,7 @@ Item { text: "Joint:"; anchors.verticalCenter: jointChooser.verticalCenter; } - ComboBox { + VrControls.ComboBox { id: jointChooser; anchors { left: jointLabel.right; leftMargin: 8; right: parent.right } model: MyAvatar.jointNames From f53aaa1d64b3c92a39bfc24e885270232287f166 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 26 Jan 2016 15:54:03 -0800 Subject: [PATCH 3/3] Removing old attachment dialog, updating menu code --- .../qml/hifi/dialogs/AttachmentsDialog.qml | 1 + interface/src/Application.cpp | 3 +- interface/src/Menu.cpp | 13 +- interface/src/ui/AttachmentsDialog.cpp | 239 ------------------ interface/src/ui/AttachmentsDialog.h | 84 ------ interface/src/ui/DialogsManager.cpp | 10 - interface/src/ui/DialogsManager.h | 1 - 7 files changed, 10 insertions(+), 341 deletions(-) delete mode 100644 interface/src/ui/AttachmentsDialog.cpp delete mode 100644 interface/src/ui/AttachmentsDialog.h diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 77771f65c4..1c70f06efd 100644 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -8,6 +8,7 @@ import "attachments" Window { id: root title: "Edit Attachments" + objectName: "AttachmentsDialog" width: 600 height: 600 resizable: true diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b3142702bb..27f9d81ab2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1845,8 +1845,7 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); offscreenUi->getRootContext()->engine()->clearComponentCache(); - offscreenUi->load("hifi/dialogs/AttachmentsDialog.qml"); - // OffscreenUi::information("Debugging", "Component cache cleared"); + OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } break; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9cbb031a61..14c91dbda8 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -70,8 +70,8 @@ Menu::Menu() { } // File > Update -- FIXME: needs implementation - auto updateAction = addActionToQMenuAndActionHash(fileMenu, "Update"); - updateAction->setDisabled(true); + auto action = addActionToQMenuAndActionHash(fileMenu, "Update"); + action->setDisabled(true); // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); @@ -166,8 +166,11 @@ Menu::Menu() { QObject* avatar = avatarManager->getMyAvatar(); // Avatar > Attachments... - addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments, 0, - dialogsManager.data(), SLOT(editAttachments())); + action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); + connect(action, &QAction::triggered, [] { + DependencyManager::get()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog"); + }); + // Avatar > Size MenuWrapper* avatarSizeMenu = avatarMenu->addMenu("Size"); @@ -285,7 +288,7 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menus", 0, false, this, SLOT(toggleDeveloperMenus())); // Settings > General... - auto action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_Comma, nullptr, nullptr, QAction::PreferencesRole); + action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_Comma, nullptr, nullptr, QAction::PreferencesRole); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog"); }); diff --git a/interface/src/ui/AttachmentsDialog.cpp b/interface/src/ui/AttachmentsDialog.cpp deleted file mode 100644 index d718b52d6d..0000000000 --- a/interface/src/ui/AttachmentsDialog.cpp +++ /dev/null @@ -1,239 +0,0 @@ -// -// AttachmentsDialog.cpp -// interface/src/ui -// -// Created by Andrzej Kapolka on 5/4/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "AttachmentsDialog.h" -#include "ModelsBrowser.h" - -AttachmentsDialog::AttachmentsDialog(QWidget* parent) : - QDialog(parent) { - - setWindowTitle("Edit Attachments"); - setAttribute(Qt::WA_DeleteOnClose); - - QVBoxLayout* layout = new QVBoxLayout(); - setLayout(layout); - - QScrollArea* area = new QScrollArea(); - layout->addWidget(area); - area->setWidgetResizable(true); - QWidget* container = new QWidget(); - container->setLayout(_attachments = new QVBoxLayout()); - container->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); - area->setWidget(container); - _attachments->addStretch(1); - - foreach (const AttachmentData& data, DependencyManager::get()->getMyAvatar()->getAttachmentData()) { - addAttachment(data); - } - - QPushButton* newAttachment = new QPushButton("New Attachment"); - connect(newAttachment, SIGNAL(clicked(bool)), SLOT(addAttachment())); - layout->addWidget(newAttachment); - - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok); - layout->addWidget(buttons); - connect(buttons, SIGNAL(accepted()), SLOT(deleteLater())); - _ok = buttons->button(QDialogButtonBox::Ok); - - setMinimumSize(600, 600); -} - -void AttachmentsDialog::setVisible(bool visible) { - QDialog::setVisible(visible); - - // un-default the OK button - if (visible) { - _ok->setDefault(false); - } -} - -void AttachmentsDialog::updateAttachmentData() { - QVector data; - for (int i = 0; i < _attachments->count() - 1; i++) { - data.append(static_cast(_attachments->itemAt(i)->widget())->getAttachmentData()); - } - DependencyManager::get()->getMyAvatar()->setAttachmentData(data); -} - -void AttachmentsDialog::addAttachment(const AttachmentData& data) { - _attachments->insertWidget(_attachments->count() - 1, new AttachmentPanel(this, data)); -} - -static QDoubleSpinBox* createTranslationBox(AttachmentPanel* panel, float value) { - QDoubleSpinBox* box = new QDoubleSpinBox(); - box->setSingleStep(0.01); - box->setMinimum(-FLT_MAX); - box->setMaximum(FLT_MAX); - box->setValue(value); - panel->connect(box, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); - return box; -} - -static QDoubleSpinBox* createRotationBox(AttachmentPanel* panel, float value) { - QDoubleSpinBox* box = new QDoubleSpinBox(); - box->setMinimum(-180.0); - box->setMaximum(180.0); - box->setWrapping(true); - box->setValue(value); - panel->connect(box, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); - return box; -} - -AttachmentPanel::AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData& data) : - _dialog(dialog), - _applying(false) { - setFrameStyle(QFrame::StyledPanel); - - QFormLayout* layout = new QFormLayout(); - layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - setLayout(layout); - - QHBoxLayout* urlBox = new QHBoxLayout(); - layout->addRow("Model URL:", urlBox); - urlBox->addWidget(_modelURL = new QLineEdit(data.modelURL.toString()), 1); - _modelURL->setText(data.modelURL.toString()); - connect(_modelURL, SIGNAL(editingFinished()), SLOT(modelURLChanged())); - QPushButton* chooseURL = new QPushButton("Choose"); - urlBox->addWidget(chooseURL); - connect(chooseURL, SIGNAL(clicked(bool)), SLOT(chooseModelURL())); - - layout->addRow("Joint:", _jointName = new QComboBox()); - QSharedPointer geometry = DependencyManager::get()->getMyAvatar()->getSkeletonModel().getGeometry(); - if (geometry && geometry->isLoaded()) { - foreach (const FBXJoint& joint, geometry->getFBXGeometry().joints) { - _jointName->addItem(joint.name); - } - } - _jointName->setCurrentText(data.jointName); - connect(_jointName, SIGNAL(currentIndexChanged(int)), SLOT(jointNameChanged())); - - QHBoxLayout* translationBox = new QHBoxLayout(); - translationBox->addWidget(_translationX = createTranslationBox(this, data.translation.x)); - translationBox->addWidget(_translationY = createTranslationBox(this, data.translation.y)); - translationBox->addWidget(_translationZ = createTranslationBox(this, data.translation.z)); - layout->addRow("Translation:", translationBox); - - QHBoxLayout* rotationBox = new QHBoxLayout(); - glm::vec3 eulers = glm::degrees(safeEulerAngles(data.rotation)); - rotationBox->addWidget(_rotationX = createRotationBox(this, eulers.x)); - rotationBox->addWidget(_rotationY = createRotationBox(this, eulers.y)); - rotationBox->addWidget(_rotationZ = createRotationBox(this, eulers.z)); - layout->addRow("Rotation:", rotationBox); - - layout->addRow("Scale:", _scale = new QDoubleSpinBox()); - _scale->setSingleStep(0.01); - _scale->setMaximum(FLT_MAX); - _scale->setValue(data.scale); - connect(_scale, SIGNAL(valueChanged(double)), SLOT(updateAttachmentData())); - - layout->addRow("Is Soft:", _isSoft = new QCheckBox()); - _isSoft->setChecked(data.isSoft); - connect(_isSoft, SIGNAL(stateChanged(int)), SLOT(updateAttachmentData())); - - QPushButton* remove = new QPushButton("Delete"); - layout->addRow(remove); - connect(remove, SIGNAL(clicked(bool)), SLOT(deleteLater())); - dialog->connect(remove, SIGNAL(clicked(bool)), SLOT(updateAttachmentData()), Qt::QueuedConnection); -} - -AttachmentData AttachmentPanel::getAttachmentData() const { - AttachmentData data; - data.modelURL = _modelURL->text(); - data.jointName = _jointName->currentText(); - data.translation = glm::vec3(_translationX->value(), _translationY->value(), _translationZ->value()); - data.rotation = glm::quat(glm::radians(glm::vec3(_rotationX->value(), _rotationY->value(), _rotationZ->value()))); - data.scale = _scale->value(); - data.isSoft = _isSoft->isChecked(); - return data; -} - -void AttachmentPanel::chooseModelURL() { - ModelsBrowser modelBrowser(FSTReader::ATTACHMENT_MODEL, this); - connect(&modelBrowser, SIGNAL(selected(QString)), SLOT(setModelURL(const QString&))); - modelBrowser.browse(); -} - -void AttachmentPanel::setModelURL(const QString& url) { - _modelURL->setText(url); - modelURLChanged(); -} - -void AttachmentPanel::modelURLChanged() { - // check for saved attachment data - if (_modelURL->text().isEmpty()) { - _dialog->updateAttachmentData(); - return; - } - AttachmentData attachment = DependencyManager::get()->getMyAvatar()->loadAttachmentData(_modelURL->text()); - if (attachment.isValid()) { - _applying = true; - _jointName->setCurrentText(attachment.jointName); - applyAttachmentData(attachment); - } - _dialog->updateAttachmentData(); -} - -void AttachmentPanel::jointNameChanged() { - if (_applying) { - return; - } - // check for saved attachment data specific to this joint - if (_modelURL->text().isEmpty()) { - _dialog->updateAttachmentData(); - return; - } - AttachmentData attachment = DependencyManager::get()->getMyAvatar()->loadAttachmentData( - _modelURL->text(), _jointName->currentText()); - if (attachment.isValid()) { - applyAttachmentData(attachment); - } - updateAttachmentData(); -} - -void AttachmentPanel::updateAttachmentData() { - if (_applying) { - return; - } - // save the attachment data under the model URL (if any) - if (!_modelURL->text().isEmpty()) { - DependencyManager::get()->getMyAvatar()->saveAttachmentData(getAttachmentData()); - } - _dialog->updateAttachmentData(); -} - -void AttachmentPanel::applyAttachmentData(const AttachmentData& attachment) { - _applying = true; - _translationX->setValue(attachment.translation.x); - _translationY->setValue(attachment.translation.y); - _translationZ->setValue(attachment.translation.z); - glm::vec3 eulers = glm::degrees(safeEulerAngles(attachment.rotation)); - _rotationX->setValue(eulers.x); - _rotationY->setValue(eulers.y); - _rotationZ->setValue(eulers.z); - _scale->setValue(attachment.scale); - _isSoft->setChecked(attachment.isSoft); - _applying = false; - _dialog->updateAttachmentData(); -} diff --git a/interface/src/ui/AttachmentsDialog.h b/interface/src/ui/AttachmentsDialog.h deleted file mode 100644 index 43ba5f8f3e..0000000000 --- a/interface/src/ui/AttachmentsDialog.h +++ /dev/null @@ -1,84 +0,0 @@ -// -// AttachmentsDialog.h -// interface/src/ui -// -// Created by Andrzej Kapolka on 5/4/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_AttachmentsDialog_h -#define hifi_AttachmentsDialog_h - -#include -#include - -#include - -class QComboBox; -class QDoubleSpinner; -class QLineEdit; -class QVBoxLayout; - -/// Allows users to edit the avatar attachments. -class AttachmentsDialog : public QDialog { - Q_OBJECT - -public: - AttachmentsDialog(QWidget* parent = nullptr); - - virtual void setVisible(bool visible); - -public slots: - - void updateAttachmentData(); - -private slots: - - void addAttachment(const AttachmentData& data = AttachmentData()); - -private: - - QVBoxLayout* _attachments; - QPushButton* _ok; -}; - -/// A panel controlling a single attachment. -class AttachmentPanel : public QFrame { - Q_OBJECT - -public: - - AttachmentPanel(AttachmentsDialog* dialog, const AttachmentData& data = AttachmentData()); - - AttachmentData getAttachmentData() const; - -private slots: - - void chooseModelURL(); - void setModelURL(const QString& url); - void modelURLChanged(); - void jointNameChanged(); - void updateAttachmentData(); - -private: - - void applyAttachmentData(const AttachmentData& attachment); - - AttachmentsDialog* _dialog; - QLineEdit* _modelURL; - QComboBox* _jointName; - QDoubleSpinBox* _translationX; - QDoubleSpinBox* _translationY; - QDoubleSpinBox* _translationZ; - QDoubleSpinBox* _rotationX; - QDoubleSpinBox* _rotationY; - QDoubleSpinBox* _rotationZ; - QDoubleSpinBox* _scale; - QCheckBox* _isSoft; - bool _applying; -}; - -#endif // hifi_AttachmentsDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index ef0ec5792f..41d7a0eb13 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,7 +19,6 @@ #include #include "AddressBarDialog.h" -#include "AttachmentsDialog.h" #include "BandwidthDialog.h" #include "CachesSizeDialog.h" #include "DiskCacheEditor.h" @@ -91,15 +90,6 @@ void DialogsManager::cachesSizeDialog() { _cachesSizeDialog->raise(); } -void DialogsManager::editAttachments() { - if (!_attachmentsDialog) { - maybeCreateDialog(_attachmentsDialog); - _attachmentsDialog->show(); - } else { - _attachmentsDialog->close(); - } -} - void DialogsManager::audioStatsDetails() { if (! _audioStatsDialog) { _audioStatsDialog = new AudioStatsDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 72a24e032c..b8fa22ec83 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -48,7 +48,6 @@ public slots: void showLoginDialog(); void octreeStatsDetails(); void cachesSizeDialog(); - void editAttachments(); void audioStatsDetails(); void bandwidthDetails(); void lodTools();