mirror of
https://github.com/lubosz/overte.git
synced 2025-04-29 15:43:19 +02:00
399 lines
12 KiB
C++
399 lines
12 KiB
C++
//
|
|
// ModelsBrowser.cpp
|
|
// interface/src/ui
|
|
//
|
|
// Created by Clement on 3/17/14.
|
|
// Copyright 2014 High Fidelity, Inc.
|
|
// Copyright 2020 Vircadia contributors.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
#include "ModelsBrowser.h"
|
|
|
|
#include <QDialog>
|
|
#include <QDialogButtonBox>
|
|
#include <QGridLayout>
|
|
#include <QFileInfo>
|
|
#include <QHeaderView>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QThread>
|
|
#include <QUrl>
|
|
#include <QUrlQuery>
|
|
#include <QXmlStreamReader>
|
|
|
|
#include <ThreadHelpers.h>
|
|
#include <NetworkAccessManager.h>
|
|
#include <NetworkingConstants.h>
|
|
#include <SharedUtil.h>
|
|
|
|
const char* MODEL_TYPE_NAMES[] = { "entities", "heads", "skeletons", "skeletons", "attachments" };
|
|
|
|
static const QString S3_URL = NetworkingConstants::HF_PUBLIC_CDN_URL;
|
|
static const QString PUBLIC_URL = "http://public.vircadia.com"; // Changed to Vircadia but not entirely sure what to do with this yet.
|
|
static const QString MODELS_LOCATION = "models/";
|
|
|
|
static const QString PREFIX_PARAMETER_NAME = "prefix";
|
|
static const QString MARKER_PARAMETER_NAME = "marker";
|
|
static const QString IS_TRUNCATED_NAME = "IsTruncated";
|
|
static const QString CONTAINER_NAME = "Contents";
|
|
static const QString KEY_NAME = "Key";
|
|
static const QString LOADING_MSG = "Loading...";
|
|
static const QString ERROR_MSG = "Error loading files";
|
|
|
|
static const QString DO_NOT_MODIFY_TAG = "DoNotModify";
|
|
|
|
enum ModelMetaData {
|
|
NAME,
|
|
CREATOR,
|
|
DATE_ADDED,
|
|
TOTAL_SIZE,
|
|
POLY_NUM,
|
|
TAGS,
|
|
|
|
MODEL_METADATA_COUNT
|
|
};
|
|
static const QString propertiesNames[MODEL_METADATA_COUNT] = {
|
|
"Name",
|
|
"Creator",
|
|
"Date Added",
|
|
"Total Size",
|
|
"Poly#",
|
|
"Tags"
|
|
};
|
|
static const QString propertiesIds[MODEL_METADATA_COUNT] = {
|
|
DO_NOT_MODIFY_TAG,
|
|
"Creator",
|
|
"Date-Added",
|
|
"Total-Size",
|
|
"Poly-Num",
|
|
"Tags"
|
|
};
|
|
|
|
ModelsBrowser::ModelsBrowser(FSTReader::ModelType modelsType, QWidget* parent) :
|
|
QWidget(parent, Qt::WindowStaysOnTopHint),
|
|
_handler(new ModelHandler(modelsType))
|
|
{
|
|
connect(_handler, SIGNAL(doneDownloading()), SLOT(resizeView()));
|
|
connect(_handler, SIGNAL(updated()), SLOT(resizeView()));
|
|
|
|
// Connect handler
|
|
_handler->connect(this, SIGNAL(startDownloading()), SLOT(download()));
|
|
_handler->connect(_handler, SIGNAL(doneDownloading()), SLOT(update()));
|
|
_handler->connect(this, SIGNAL(destroyed()), SLOT(exit()));
|
|
|
|
// Setup and launch update thread
|
|
moveToNewNamedThread(_handler, "Models Browser");
|
|
emit startDownloading();
|
|
|
|
// Initialize the view
|
|
_view.setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
_view.setRootIsDecorated(false);
|
|
_view.setModel(_handler->getModel());
|
|
_view.blockSignals(true);
|
|
|
|
// Initialize the search bar
|
|
_searchBar = new QLineEdit;
|
|
_searchBar->setDisabled(true);
|
|
connect(_handler, SIGNAL(doneDownloading()), SLOT(enableSearchBar()));
|
|
}
|
|
|
|
void ModelsBrowser::applyFilter(const QString &filter) {
|
|
QStringList filters = filter.split(" ");
|
|
|
|
_handler->lockModel();
|
|
QStandardItemModel* model = _handler->getModel();
|
|
int rows = model->rowCount();
|
|
|
|
// Try and match every filter with each rows
|
|
for (int i = 0; i < rows; ++i) {
|
|
bool match = false;
|
|
for (int k = 0; k < filters.count(); ++k) {
|
|
match = false;
|
|
for (int j = 0; j < MODEL_METADATA_COUNT; ++j) {
|
|
if (model->item(i, j)->text().contains(filters.at(k), Qt::CaseInsensitive)) {
|
|
match = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!match) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Hid the row if it doesn't match (Make sure it's not it it does)
|
|
_view.setRowHidden(i, QModelIndex(), !match);
|
|
}
|
|
_handler->unlockModel();
|
|
}
|
|
|
|
void ModelsBrowser::resizeView() {
|
|
for (int i = 0; i < MODEL_METADATA_COUNT; ++i) {
|
|
_view.resizeColumnToContents(i);
|
|
}
|
|
}
|
|
|
|
void ModelsBrowser::enableSearchBar() {
|
|
_view.blockSignals(false);
|
|
_searchBar->setEnabled(true);
|
|
}
|
|
|
|
void ModelsBrowser::setNameFilter(QString nameFilter) {
|
|
_handler->setNameFilter(nameFilter);
|
|
}
|
|
|
|
void ModelsBrowser::browse() {
|
|
_selectedFile = "";
|
|
|
|
QDialog dialog;
|
|
dialog.setWindowTitle("Browse models");
|
|
dialog.setMinimumSize(570, 500);
|
|
|
|
QGridLayout* layout = new QGridLayout(&dialog);
|
|
dialog.setLayout(layout);
|
|
|
|
layout->addWidget(_searchBar, 0, 0);
|
|
layout->addWidget(&_view, 1, 0);
|
|
dialog.connect(&_view, SIGNAL(doubleClicked(const QModelIndex&)), SLOT(accept()));
|
|
connect(_searchBar, SIGNAL(textChanged(const QString&)), SLOT(applyFilter(const QString&)));
|
|
|
|
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
|
layout->addWidget(buttons, 2, 0);
|
|
dialog.connect(buttons, SIGNAL(accepted()), SLOT(accept()));
|
|
dialog.connect(buttons, SIGNAL(rejected()), SLOT(reject()));
|
|
|
|
QVariant selectedFile;
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
_handler->lockModel();
|
|
selectedFile = _handler->getModel()->data(_view.currentIndex(), Qt::UserRole);
|
|
_handler->unlockModel();
|
|
if (selectedFile.isValid()) {
|
|
_selectedFile = selectedFile.toString();
|
|
}
|
|
}
|
|
emit selected(_selectedFile);
|
|
|
|
// So that we don't have to reconstruct the view
|
|
_view.setParent(NULL);
|
|
}
|
|
|
|
|
|
ModelHandler::ModelHandler(FSTReader::ModelType modelsType, QWidget* parent) :
|
|
QObject(parent),
|
|
_initiateExit(false),
|
|
_type(modelsType),
|
|
_nameFilter(".*fst")
|
|
{
|
|
// set headers data
|
|
QStringList headerData;
|
|
for (int i = 0; i < MODEL_METADATA_COUNT; ++i) {
|
|
headerData << propertiesNames[i];
|
|
}
|
|
_model.setHorizontalHeaderLabels(headerData);
|
|
}
|
|
|
|
void ModelHandler::setNameFilter(QString nameFilter) {
|
|
_nameFilter = nameFilter;
|
|
}
|
|
|
|
void ModelHandler::download() {
|
|
_lock.lockForWrite();
|
|
if (_initiateExit) {
|
|
_lock.unlock();
|
|
return;
|
|
}
|
|
// Show loading message
|
|
QStandardItem* loadingItem = new QStandardItem(LOADING_MSG);
|
|
loadingItem->setEnabled(false);
|
|
_model.appendRow(loadingItem);
|
|
_lock.unlock();
|
|
|
|
// Query models list
|
|
queryNewFiles();
|
|
}
|
|
|
|
void ModelHandler::update() {
|
|
_lock.lockForWrite();
|
|
if (_initiateExit) {
|
|
_lock.unlock();
|
|
return;
|
|
}
|
|
for (int i = 0; i < _model.rowCount(); ++i) {
|
|
QUrl url(_model.item(i,0)->data(Qt::UserRole).toString());
|
|
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
|
QNetworkRequest request(url);
|
|
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT);
|
|
QNetworkReply* reply = networkAccessManager.head(request);
|
|
connect(reply, SIGNAL(finished()), SLOT(downloadFinished()));
|
|
}
|
|
_lock.unlock();
|
|
}
|
|
|
|
void ModelHandler::exit() {
|
|
_lock.lockForWrite();
|
|
_initiateExit = true;
|
|
|
|
// Disconnect everything
|
|
disconnect();
|
|
thread()->disconnect();
|
|
|
|
// Make sure the thread will exit correctly
|
|
thread()->connect(this, SIGNAL(destroyed()), SLOT(quit()));
|
|
thread()->connect(thread(), SIGNAL(finished()), SLOT(deleteLater()));
|
|
deleteLater();
|
|
_lock.unlock();
|
|
}
|
|
|
|
void ModelHandler::downloadFinished() {
|
|
QNetworkReply* reply = static_cast<QNetworkReply*>(sender());
|
|
QByteArray data = reply->readAll();
|
|
|
|
if (!data.isEmpty()) {
|
|
parseXML(data);
|
|
} else {
|
|
parseHeaders(reply);
|
|
}
|
|
reply->deleteLater();
|
|
}
|
|
|
|
void ModelHandler::queryNewFiles(QString marker) {
|
|
if (_initiateExit) {
|
|
return;
|
|
}
|
|
|
|
// Build query
|
|
QUrl url(S3_URL);
|
|
QUrlQuery query;
|
|
query.addQueryItem(PREFIX_PARAMETER_NAME, MODELS_LOCATION + MODEL_TYPE_NAMES[_type]);
|
|
|
|
if (!marker.isEmpty()) {
|
|
query.addQueryItem(MARKER_PARAMETER_NAME, marker);
|
|
}
|
|
|
|
// Download
|
|
url.setQuery(query);
|
|
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
|
QNetworkRequest request(url);
|
|
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
|
request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT);
|
|
QNetworkReply* reply = networkAccessManager.get(request);
|
|
connect(reply, SIGNAL(finished()), SLOT(downloadFinished()));
|
|
|
|
}
|
|
|
|
bool ModelHandler::parseXML(QByteArray xmlFile) {
|
|
_lock.lockForWrite();
|
|
if (_initiateExit) {
|
|
_lock.unlock();
|
|
return false;
|
|
}
|
|
|
|
QXmlStreamReader xml(xmlFile);
|
|
QRegExp rx(_nameFilter);
|
|
bool truncated = false;
|
|
QString lastKey;
|
|
|
|
// Remove loading indication
|
|
int oldLastRow = _model.rowCount() - 1;
|
|
delete _model.takeRow(oldLastRow).first();
|
|
|
|
// Read xml until the end or an error is detected
|
|
while(!xml.atEnd() && !xml.hasError()) {
|
|
if (_initiateExit) {
|
|
_lock.unlock();
|
|
return false;
|
|
}
|
|
|
|
if(xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) {
|
|
while(!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) {
|
|
// Let's check if there is more
|
|
xml.readNext();
|
|
if (xml.text().toString() == "True") {
|
|
truncated = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) {
|
|
while(!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) {
|
|
// If a file is find, process it
|
|
if(xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) {
|
|
xml.readNext();
|
|
lastKey = xml.text().toString();
|
|
if (rx.exactMatch(xml.text().toString())) {
|
|
// Add the found file to the list
|
|
QList<QStandardItem*> model;
|
|
model << new QStandardItem(QFileInfo(xml.text().toString()).baseName());
|
|
model.first()->setData(PUBLIC_URL + "/" + xml.text().toString(), Qt::UserRole);
|
|
for (int i = 1; i < MODEL_METADATA_COUNT; ++i) {
|
|
model << new QStandardItem();
|
|
}
|
|
|
|
_model.appendRow(model);
|
|
}
|
|
}
|
|
xml.readNext();
|
|
}
|
|
}
|
|
xml.readNext();
|
|
}
|
|
|
|
// Error handling
|
|
if(xml.hasError()) {
|
|
_model.clear();
|
|
QStandardItem* errorItem = new QStandardItem(ERROR_MSG);
|
|
errorItem->setEnabled(false);
|
|
_model.appendRow(errorItem);
|
|
|
|
_lock.unlock();
|
|
return false;
|
|
}
|
|
|
|
// If we didn't all the files, download the next ones
|
|
if (truncated) {
|
|
// Indicate more files are being loaded
|
|
QStandardItem* loadingItem = new QStandardItem(LOADING_MSG);
|
|
loadingItem->setEnabled(false);
|
|
_model.appendRow(loadingItem);
|
|
|
|
// query those files
|
|
queryNewFiles(lastKey);
|
|
}
|
|
_lock.unlock();
|
|
|
|
if (!truncated) {
|
|
emit doneDownloading();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ModelHandler::parseHeaders(QNetworkReply* reply) {
|
|
_lock.lockForWrite();
|
|
|
|
QList<QStandardItem*> items = _model.findItems(QFileInfo(reply->url().toString()).baseName());
|
|
if (items.isEmpty() || items.first()->text() == DO_NOT_MODIFY_TAG) {
|
|
_lock.unlock();
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < MODEL_METADATA_COUNT; ++i) {
|
|
for (int k = 1; k < reply->rawHeaderPairs().count(); ++k) {
|
|
QString key = reply->rawHeaderPairs().at(k).first.data();
|
|
QString item = reply->rawHeaderPairs().at(k).second.data();
|
|
if (key == propertiesIds[i]) {
|
|
_model.item(_model.indexFromItem(items.first()).row(), i)->setText(item);
|
|
}
|
|
}
|
|
}
|
|
_lock.unlock();
|
|
|
|
emit updated();
|
|
return true;
|
|
}
|
|
|