diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 402347c5d4..dba5feca9e 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -91,6 +91,7 @@ Menu::Menu() : _jsConsole(NULL), _octreeStatsDialog(NULL), _lodToolsDialog(NULL), + _userLocationsDialog(NULL), _maxVoxels(DEFAULT_MAX_VOXELS_PER_SYSTEM), _voxelSizeScale(DEFAULT_OCTREE_SIZE_SCALE), _oculusUIAngularSize(DEFAULT_OCULUS_UI_ANGULAR_SIZE), @@ -166,6 +167,11 @@ Menu::Menu() : Qt::CTRL | Qt::Key_N, this, SLOT(nameLocation())); + addActionToQMenuAndActionHash(fileMenu, + MenuOption::MyLocations, + Qt::CTRL | Qt::Key_K, + this, + SLOT(toggleLocationList())); addActionToQMenuAndActionHash(fileMenu, MenuOption::GoTo, Qt::Key_At, @@ -1184,6 +1190,17 @@ void Menu::namedLocationCreated(LocationManager::NamedLocationCreateResponse res msgBox.exec(); } +void Menu::toggleLocationList() { + if (!_userLocationsDialog) { + _userLocationsDialog = new UserLocationsDialog(Application::getInstance()->getWindow()); + } + if (_userLocationsDialog->isVisible()) { + _userLocationsDialog->hide(); + } else { + _userLocationsDialog->show(); + } +} + void Menu::nameLocation() { // check if user is logged in or show login dialog if not diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a15d3712f1..06b5c5c9f4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -29,6 +29,7 @@ #include "ui/JSConsole.h" #include "ui/LoginDialog.h" #include "ui/ScriptEditorWindow.h" +#include "ui/UserLocationsDialog.h" const float ADJUST_LOD_DOWN_FPS = 40.0; const float ADJUST_LOD_UP_FPS = 55.0; @@ -199,6 +200,7 @@ private slots: void goToDomainDialog(); void goToLocation(); void nameLocation(); + void toggleLocationList(); void bandwidthDetailsClosed(); void octreeStatsDetailsClosed(); void lodToolsClosed(); @@ -265,6 +267,7 @@ private: QDialog* _jsConsole; OctreeStatsDialog* _octreeStatsDialog; LodToolsDialog* _lodToolsDialog; + UserLocationsDialog* _userLocationsDialog; int _maxVoxels; float _voxelSizeScale; float _oculusUIAngularSize; @@ -395,6 +398,7 @@ namespace MenuOption { const QString MoveWithLean = "Move with Lean"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; + const QString MyLocations = "My Locations..."; const QString NameLocation = "Name this location"; const QString NewVoxelCullingMode = "New Voxel Culling Mode"; const QString OctreeStats = "Voxel and Particle Statistics"; diff --git a/interface/src/UserLocationsModel.cpp b/interface/src/UserLocationsModel.cpp new file mode 100644 index 0000000000..e84cae8f95 --- /dev/null +++ b/interface/src/UserLocationsModel.cpp @@ -0,0 +1,246 @@ +// +// UserLocationsModel.cpp +// interface/src +// +// Created by Ryan Huffman on 06/24/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 "AccountManager.h" +#include "Application.h" +#include "UserLocationsModel.h" + +static const QString PLACES_GET = "/api/v1/places"; +static const QString PLACES_UPDATE = "/api/v1/places/%1"; +static const QString PLACES_DELETE= "/api/v1/places/%1"; + +UserLocation::UserLocation(QString id, QString name, QString location) : + _id(id), + _name(name), + _location(location), + _previousName(name), + _updating(false) { +} + +void UserLocation::requestRename(const QString& newName) { + if (!_updating && newName.toLower() != _name) { + _updating = true; + + JSONCallbackParameters callbackParams(this, "handleRenameResponse", this, "handleRenameError"); + QJsonObject jsonNameObject; + jsonNameObject.insert("name", QJsonValue(newName)); + QJsonDocument jsonDocument(jsonNameObject); + AccountManager::getInstance().authenticatedRequest(PLACES_UPDATE.arg(_id), + QNetworkAccessManager::PutOperation, + callbackParams, + jsonDocument.toJson()); + _previousName = _name; + _name = newName; + + emit updated(_name); + } +} + +void UserLocation::handleRenameResponse(const QJsonObject& responseData) { + _updating = false; + + QJsonValue status = responseData["status"]; + if (!status.isUndefined() && status.toString() == "success") { + QString updatedName = responseData["data"].toObject()["name"].toString(); + _name = updatedName; + } else { + _name = _previousName; + + QString msg = "There was an error renaming location '" + _name + "'"; + + QJsonValue data = responseData["data"]; + if (!data.isUndefined()) { + QJsonValue nameError = data.toObject()["name"]; + if (!nameError.isUndefined()) { + msg += ": " + nameError.toString(); + } + } + qDebug() << msg; + QMessageBox::warning(Application::getInstance()->getWindow(), "Error", msg); + } + + emit updated(_name); +} + +void UserLocation::handleRenameError(QNetworkReply::NetworkError error, const QString& errorString) { + _updating = false; + + QString msg = "There was an error renaming location '" + _name + "': " + errorString; + qDebug() << msg; + QMessageBox::warning(Application::getInstance()->getWindow(), "Error", msg); + + emit updated(_name); +} + +void UserLocation::requestDelete() { + if (!_updating) { + _updating = true; + + JSONCallbackParameters callbackParams(this, "handleDeleteResponse", this, "handleDeleteError"); + AccountManager::getInstance().authenticatedRequest(PLACES_DELETE.arg(_id), + QNetworkAccessManager::DeleteOperation, + callbackParams); + } +} + +void UserLocation::handleDeleteResponse(const QJsonObject& responseData) { + _updating = false; + + QJsonValue status = responseData["status"]; + if (!status.isUndefined() && status.toString() == "success") { + emit deleted(_name); + } else { + QString msg = "There was an error deleting location '" + _name + "'"; + qDebug() << msg; + QMessageBox::warning(Application::getInstance()->getWindow(), "Error", msg); + } +} + +void UserLocation::handleDeleteError(QNetworkReply::NetworkError error, const QString& errorString) { + _updating = false; + + QString msg = "There was an error deleting location '" + _name + "': " + errorString; + qDebug() << msg; + QMessageBox::warning(Application::getInstance()->getWindow(), "Error", msg); +} + +UserLocationsModel::UserLocationsModel(QObject* parent) : + QAbstractListModel(parent), + _updating(false) { + + refresh(); +} + +UserLocationsModel::~UserLocationsModel() { + qDeleteAll(_locations); + _locations.clear(); +} + +void UserLocationsModel::update() { + beginResetModel(); + endResetModel(); +} + +void UserLocationsModel::deleteLocation(const QModelIndex& index) { + UserLocation* location = _locations[index.row()]; + location->requestDelete(); +} + +void UserLocationsModel::renameLocation(const QModelIndex& index, const QString& newName) { + UserLocation* location = _locations[index.row()]; + location->requestRename(newName); +} + +void UserLocationsModel::refresh() { + if (!_updating) { + beginResetModel(); + qDeleteAll(_locations); + _locations.clear(); + _updating = true; + endResetModel(); + + JSONCallbackParameters callbackParams(this, "handleLocationsResponse"); + AccountManager::getInstance().authenticatedRequest(PLACES_GET, + QNetworkAccessManager::GetOperation, + callbackParams); + } +} + +void UserLocationsModel::handleLocationsResponse(const QJsonObject& responseData) { + _updating = false; + + QJsonValue status = responseData["status"]; + if (!status.isUndefined() && status.toString() == "success") { + beginResetModel(); + QJsonArray locations = responseData["data"].toObject()["places"].toArray(); + for (QJsonArray::const_iterator it = locations.constBegin(); it != locations.constEnd(); it++) { + QJsonObject location = (*it).toObject(); + QJsonObject address = location["address"].toObject(); + UserLocation* userLocation = new UserLocation(location["id"].toString(), location["name"].toString(), + "hifi://" + address["domain"].toString() + + "/" + address["position"].toString() + + "/" + address["orientation"].toString()); + _locations.append(userLocation); + connect(userLocation, &UserLocation::deleted, this, &UserLocationsModel::removeLocation); + connect(userLocation, &UserLocation::updated, this, &UserLocationsModel::update); + } + endResetModel(); + } else { + qDebug() << "Error loading location data"; + } +} + +void UserLocationsModel::removeLocation(const QString& name) { + beginResetModel(); + for (QList::iterator it = _locations.begin(); it != _locations.end(); it++) { + if ((*it)->name() == name) { + _locations.erase(it); + break; + } + } + endResetModel(); +} + +int UserLocationsModel::rowCount(const QModelIndex& parent) const { + if (parent.isValid()) { + return 0; + } + + if (_updating) { + return 1; + } + + return _locations.length(); +} + +QVariant UserLocationsModel::data(const QModelIndex& index, int role) const { + if (role == Qt::DisplayRole) { + if (_updating) { + return QVariant("Updating..."); + } else if (index.row() > _locations.length()) { + return QVariant(); + } else if (index.column() == NameColumn) { + return _locations[index.row()]->name(); + } else if (index.column() == LocationColumn) { + return QVariant(_locations[index.row()]->location()); + } + } + + return QVariant(); + +} +QVariant UserLocationsModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case NameColumn: return "Name"; + case LocationColumn: return "Location"; + default: return QVariant(); + } + } + + return QVariant(); +} + +Qt::ItemFlags UserLocationsModel::flags(const QModelIndex& index) const { + if (index.row() < _locations.length()) { + UserLocation* ul = _locations[index.row()]; + if (ul->isUpdating()) { + return Qt::NoItemFlags; + } + } + + return QAbstractListModel::flags(index); +} diff --git a/interface/src/UserLocationsModel.h b/interface/src/UserLocationsModel.h new file mode 100644 index 0000000000..d3f86faa5a --- /dev/null +++ b/interface/src/UserLocationsModel.h @@ -0,0 +1,82 @@ +// +// UserLocationsModel.h +// interface/src +// +// Created by Ryan Huffman on 06/24/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_UserLocationsModel_h +#define hifi_UserLocationsModel_h + +#include +#include +#include + + +class UserLocation : public QObject { + Q_OBJECT +public: + UserLocation(QString id, QString name, QString location); + bool isUpdating() { return _updating; } + void requestRename(const QString& newName); + void requestDelete(); + + QString id() { return _id; } + QString name() { return _name; } + QString location() { return _location; } + +public slots: + void handleRenameResponse(const QJsonObject& responseData); + void handleRenameError(QNetworkReply::NetworkError error, const QString& errorString); + void handleDeleteResponse(const QJsonObject& responseData); + void handleDeleteError(QNetworkReply::NetworkError error, const QString& errorString); + +signals: + void updated(const QString& name); + void deleted(const QString& name); + +private: + QString _id; + QString _name; + QString _location; + QString _previousName; + bool _updating; + +}; + +class UserLocationsModel : public QAbstractListModel { + Q_OBJECT +public: + UserLocationsModel(QObject* parent = NULL); + ~UserLocationsModel(); + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const; + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex& parent = QModelIndex()) const { return 2; }; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + + void deleteLocation(const QModelIndex& index); + void renameLocation(const QModelIndex& index, const QString& newName); + + enum Columns { + NameColumn = 0, + LocationColumn + }; + +public slots: + void refresh(); + void update(); + void handleLocationsResponse(const QJsonObject& responseData); + void removeLocation(const QString& name); + +private: + bool _updating; + QList _locations; +}; + +#endif // hifi_UserLocationsModel_h diff --git a/interface/src/ui/UserLocationsDialog.cpp b/interface/src/ui/UserLocationsDialog.cpp new file mode 100644 index 0000000000..f72e66ce77 --- /dev/null +++ b/interface/src/ui/UserLocationsDialog.cpp @@ -0,0 +1,77 @@ +// +// UserLocationsDialog.cpp +// interface/src/ui +// +// Created by Ryan Huffman on 06/24/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 "Menu.h" +#include "UserLocationsDialog.h" + +UserLocationsDialog::UserLocationsDialog(QWidget* parent) : + QDialog(parent), + _ui(), + _proxyModel(this), + _userLocationsModel(this) { + + _ui.setupUi(this); + + _proxyModel.setSourceModel(&_userLocationsModel); + _proxyModel.setDynamicSortFilter(true); + + _ui.locationsTreeView->setModel(&_proxyModel); + _ui.locationsTreeView->setSortingEnabled(true); + _ui.locationsTreeView->sortByColumn(UserLocationsModel::NameColumn, Qt::AscendingOrder); + + connect(_ui.locationsTreeView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &UserLocationsDialog::updateEnabled); + connect(&_userLocationsModel, &UserLocationsModel::modelReset, this, &UserLocationsDialog::updateEnabled); + connect(&_userLocationsModel, &UserLocationsModel::modelReset, &_proxyModel, &QSortFilterProxyModel::invalidate); + connect(_ui.locationsTreeView, &QTreeView::doubleClicked, this, &UserLocationsDialog::goToModelIndex); + + connect(_ui.deleteButton, &QPushButton::clicked, this, &UserLocationsDialog::deleteSelection); + connect(_ui.renameButton, &QPushButton::clicked, this, &UserLocationsDialog::renameSelection); + connect(_ui.refreshButton, &QPushButton::clicked, &_userLocationsModel, &UserLocationsModel::refresh); + + this->setWindowTitle("My Locations"); +} + +void UserLocationsDialog::updateEnabled() { + bool enabled = _ui.locationsTreeView->selectionModel()->hasSelection(); + _ui.renameButton->setEnabled(enabled); + _ui.deleteButton->setEnabled(enabled); +} + +void UserLocationsDialog::goToModelIndex(const QModelIndex& index) { + QVariant location = _proxyModel.data(index.sibling(index.row(), UserLocationsModel::LocationColumn)); + Menu::getInstance()->goToURL(location.toString()); +} + +void UserLocationsDialog::deleteSelection() { + QModelIndex selection = _ui.locationsTreeView->selectionModel()->currentIndex(); + selection = _proxyModel.mapToSource(selection); + if (selection.isValid()) { + _userLocationsModel.deleteLocation(selection); + } +} + +void UserLocationsDialog::renameSelection() { + QModelIndex selection = _ui.locationsTreeView->selectionModel()->currentIndex(); + selection = _proxyModel.mapToSource(selection); + if (selection.isValid()) { + bool ok; + QString name = _userLocationsModel.data(selection.sibling(selection.row(), UserLocationsModel::NameColumn)).toString(); + QString newName = QInputDialog::getText(this, "Rename '" + name + "'", "Set name to:", QLineEdit::Normal, name, &ok); + if (ok && !newName.isEmpty()) { + _userLocationsModel.renameLocation(selection, newName); + } + } +} diff --git a/interface/src/ui/UserLocationsDialog.h b/interface/src/ui/UserLocationsDialog.h new file mode 100644 index 0000000000..0e596ece87 --- /dev/null +++ b/interface/src/ui/UserLocationsDialog.h @@ -0,0 +1,35 @@ +// +// UserLocationsDialog.h +// interface/src/ui +// +// Created by Ryan Huffman on 06/24/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_UserLocationsDialog_h +#define hifi_UserLocationsDialog_h + +#include "ui_userLocationsDialog.h" +#include "UserLocationsModel.h" + +class UserLocationsDialog : public QDialog { + Q_OBJECT +public: + UserLocationsDialog(QWidget* parent = NULL); + +protected slots: + void updateEnabled(); + void goToModelIndex(const QModelIndex& index); + void deleteSelection(); + void renameSelection(); + +private: + Ui::UserLocationsDialog _ui; + QSortFilterProxyModel _proxyModel; + UserLocationsModel _userLocationsModel; +}; + +#endif // hifi_UserLocationsDialog_h diff --git a/interface/ui/userLocationsDialog.ui b/interface/ui/userLocationsDialog.ui new file mode 100644 index 0000000000..609ce1c8ab --- /dev/null +++ b/interface/ui/userLocationsDialog.ui @@ -0,0 +1,130 @@ + + + UserLocationsDialog + + + + 0 + 0 + 929 + 633 + + + + Form + + + + -1 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + + + font-size: 16px + + + My Locations + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh + + + + + + + + + + 0 + + + false + + + + + + + + -1 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Rename + + + + + + + Delete + + + + + + + + + + + diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 918261a953..ce138e144e 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -37,13 +37,15 @@ Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; -JSONCallbackParameters::JSONCallbackParameters() : - jsonCallbackReceiver(NULL), - jsonCallbackMethod(), - errorCallbackReceiver(NULL), - errorCallbackMethod(), - updateReciever(NULL), - updateSlot() +JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, + QObject* errorCallbackReceiver, const QString& errorCallbackMethod, + QObject* updateReceiver, const QString& updateSlot) : + jsonCallbackReceiver(jsonCallbackReceiver), + jsonCallbackMethod(jsonCallbackMethod), + errorCallbackReceiver(errorCallbackReceiver), + errorCallbackMethod(errorCallbackMethod), + updateReciever(updateReceiver), + updateSlot(updateSlot) { } @@ -204,6 +206,9 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager:: } } + break; + case QNetworkAccessManager::DeleteOperation: + networkReply = _networkAccessManager->sendCustomRequest(authenticatedRequest, "DELETE"); break; default: // other methods not yet handled diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index c18836ca54..389f84e01f 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -22,7 +22,9 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(); + JSONCallbackParameters(QObject* jsonCallbackReceiver = NULL, const QString& jsonCallbackMethod = QString(), + QObject* errorCallbackReceiver = NULL, const QString& errorCallbackMethod = QString(), + QObject* updateReceiver = NULL, const QString& updateSlot = QString()); bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; }