Merge pull request #6943 from jherico/attachments

Attachment dialog in overlay
This commit is contained in:
Brad Hefta-Gaub 2016-01-26 22:51:40 -08:00
commit dc6662d115
20 changed files with 931 additions and 340 deletions

View file

@ -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()
}
}
}
}
}
}

View file

@ -0,0 +1,124 @@
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"
objectName: "AttachmentsDialog"
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()
}
}
}
}

View file

@ -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();
}
}
}
}
}

View file

@ -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();
}
}
}
}
}

View file

@ -0,0 +1,165 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../../windows"
import "../../../controls" as VrControls
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;
}
VrControls.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);
}
}
}

View file

@ -0,0 +1,9 @@
import "."
Vector3 {
decimals: 1;
stepSize: 1;
maximumValue: 180
minimumValue: -180
}

View file

@ -0,0 +1,9 @@
import "."
Vector3 {
decimals: 2;
stepSize: 0.01;
maximumValue: 10
minimumValue: -10
}

View file

@ -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();
}
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.0
import QtQuick.XmlListModel 2.0
//http://s3.amazonaws.com/hifi-public?prefix=models/attachments
/*
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Contents>
<Key>models/attachments/guitar.fst</Key>
<LastModified>2015-11-10T00:28:22.000Z</LastModified>
<ETag>&quot;236c00c4802ba9c2605cabd5601d138e&quot;</ETag>
<Size>2992</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
</ListBucketResult>
*/
// 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()" }
}

View file

@ -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];
}

View file

@ -1186,6 +1186,7 @@ void Application::initializeUi() {
UpdateDialog::registerType();
qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->create(_offscreenContext->getContext());
offscreenUi->setProxyWindow(_window->windowHandle());
@ -1842,6 +1843,9 @@ void Application::keyPressEvent(QKeyEvent* event) {
case Qt::Key_X:
if (isShifted && isMeta) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->getRootContext()->engine()->clearComponentCache();
OffscreenUi::information("Debugging", "Component cache cleared");
// placeholder for dialogs being converted to QML.
}
break;

View file

@ -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<OffscreenUi>()->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<OffscreenUi>()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog");
});

View file

@ -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 <QComboBox>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QCheckBox>
#include <avatar/AvatarManager.h>
#include <avatar/MyAvatar.h>
#include <DependencyManager.h>
#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<AvatarManager>()->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<AttachmentData> data;
for (int i = 0; i < _attachments->count() - 1; i++) {
data.append(static_cast<AttachmentPanel*>(_attachments->itemAt(i)->widget())->getAttachmentData());
}
DependencyManager::get<AvatarManager>()->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<NetworkGeometry> geometry = DependencyManager::get<AvatarManager>()->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<AvatarManager>()->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<AvatarManager>()->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<AvatarManager>()->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();
}

View file

@ -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 <QDialog>
#include <QFrame>
#include <AvatarData.h>
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

View file

@ -19,7 +19,6 @@
#include <PathUtils.h>
#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());

View file

@ -48,7 +48,6 @@ public slots:
void showLoginDialog();
void octreeStatsDetails();
void cachesSizeDialog();
void editAttachments();
void audioStatsDetails();
void bandwidthDetails();
void lodTools();

View file

@ -25,6 +25,7 @@
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include <QVariantGLM.h>
#include <Transform.h>
#include <NetworkAccessManager.h>
#include <NodeList.h>
@ -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<AttachmentData> newAttachments;
newAttachments.reserve(variant.size());
for (const auto& attachmentVar : variant) {
AttachmentData attachment;
attachment.fromVariant(attachmentVar);
newAttachments.append(attachment);
}
setAttachmentData(newAttachments);
}

View file

@ -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);

View file

@ -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();

View file

@ -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{}
}
}
}