From b897bac235a95f9c4654b31d101779bd0cc8f72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remko=20Tron=C3=A7on?= Date: Fri, 17 Dec 2010 14:21:14 +0100 Subject: Added profile edit dialog. Resolves: #141, #587. diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index f07a964..d7e1941 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -61,6 +61,7 @@ #include "Swift/Controllers/CertificateStorageFactory.h" #include "Swift/Controllers/CertificateStorageTrustChecker.h" #include "Swiften/Network/NetworkFactories.h" +#include namespace Swift { @@ -98,6 +99,7 @@ MainController::MainController( rosterController_ = NULL; chatsManager_ = NULL; eventWindowController_ = NULL; + profileController_ = NULL; userSearchControllerChat_ = NULL; userSearchControllerAdd_ = NULL; quitRequested_ = false; @@ -170,6 +172,8 @@ MainController::~MainController() { void MainController::resetClient() { resetCurrentError(); resetPendingReconnects(); + delete profileController_; + profileController_ = NULL; delete eventWindowController_; eventWindowController_ = NULL; delete chatsManager_; @@ -228,6 +232,8 @@ void MainController::handleConnected() { bool freshLogin = rosterController_ == NULL; myStatusLooksOnline_ = true; if (freshLogin) { + profileController_ = new ProfileController(client_->getVCardManager(), uiFactory_, uiEventStream_); + rosterController_ = new RosterController(jid_, client_->getRoster(), client_->getAvatarManager(), uiFactory_, client_->getNickManager(), client_->getNickResolver(), client_->getPresenceOracle(), client_->getSubscriptionManager(), eventController_, uiEventStream_, client_->getIQRouter(), settings_); rosterController_->onChangeStatusRequest.connect(boost::bind(&MainController::handleChangeStatusRequest, this, _1, _2)); rosterController_->onSignOutRequest.connect(boost::bind(&MainController::signOut, this)); @@ -261,6 +267,7 @@ void MainController::handleConnected() { client_->getVCardManager()->requestOwnVCard(); rosterController_->setEnabled(true); + profileController_->setAvailable(true); /* Send presence later to catch all the incoming presences. */ sendPresence(statusTracker_->getNextPresence()); /* Enable chats last of all, so rejoining MUCs has the right sent presence */ @@ -522,6 +529,9 @@ void MainController::setManagersOffline() { if (rosterController_) { rosterController_->setEnabled(false); } + if (profileController_) { + profileController_->setAvailable(false); + } } void MainController::handleServerDiscoInfoResponse(boost::shared_ptr info, ErrorPayload::ref error) { diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h index a933a5a..d6e54ef 100644 --- a/Swift/Controllers/MainController.h +++ b/Swift/Controllers/MainController.h @@ -42,6 +42,7 @@ namespace Swift { class EventLoop; class MUCController; class Notifier; + class ProfileController; class TogglableNotifier; class PresenceNotifier; class EventNotifier; @@ -130,6 +131,7 @@ namespace Swift { UIEventStream* uiEventStream_; XMLConsoleController* xmlConsoleController_; ChatsManager* chatsManager_; + ProfileController* profileController_; JID jid_; JID boundJID_; SystemTrayController* systemTrayController_; diff --git a/Swift/Controllers/ProfileController.cpp b/Swift/Controllers/ProfileController.cpp new file mode 100644 index 0000000..9667d89 --- /dev/null +++ b/Swift/Controllers/ProfileController.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include + +#include + +#include +#include +#include +#include + + +namespace Swift { + +ProfileController::ProfileController(VCardManager* vcardManager, ProfileWindowFactory* profileWindowFactory, UIEventStream* uiEventStream) : vcardManager(vcardManager), profileWindowFactory(profileWindowFactory), uiEventStream(uiEventStream), available(true), profileWindow(NULL), gettingVCard(false) { + uiEventStream->onUIEvent.connect(boost::bind(&ProfileController::handleUIEvent, this, _1)); +} + +ProfileController::~ProfileController() { + if (profileWindow) { + vcardManager->onOwnVCardChanged.disconnect(boost::bind(&ProfileController::handleOwnVCardChanged, this, _1)); + profileWindow->onVCardChangeRequest.disconnect(boost::bind(&ProfileController::handleVCardChangeRequest, this, _1)); + delete profileWindow; + } + uiEventStream->onUIEvent.disconnect(boost::bind(&ProfileController::handleUIEvent, this, _1)); +} + +void ProfileController::handleUIEvent(UIEvent::ref event) { + if (!boost::dynamic_pointer_cast(event)) { + return; + } + + if (!profileWindow) { + profileWindow = profileWindowFactory->createProfileWindow(); + profileWindow->onVCardChangeRequest.connect(boost::bind(&ProfileController::handleVCardChangeRequest, this, _1)); + vcardManager->onOwnVCardChanged.connect(boost::bind(&ProfileController::handleOwnVCardChanged, this, _1)); + } + gettingVCard = true; + updateDialogStatus(); + vcardManager->requestOwnVCard(); + profileWindow->show(); +} + +void ProfileController::handleVCardChangeRequest(VCard::ref vcard) { + assert(!pendingSetVCardRequest); + profileWindow->setError(""); + pendingSetVCardRequest = vcardManager->createSetVCardRequest(vcard); + pendingSetVCardRequest->onResponse.connect(boost::bind(&ProfileController::handleSetVCardResponse, this, _2)); + pendingSetVCardRequest->send(); + updateDialogStatus(); +} + +void ProfileController::handleSetVCardResponse(ErrorPayload::ref error) { + pendingSetVCardRequest.reset(); + updateDialogStatus(); + if (error) { + profileWindow->setError("There was an error publishing your profile data"); + } + else { + profileWindow->setError(""); + profileWindow->hide(); + } +} + +void ProfileController::handleOwnVCardChanged(VCard::ref vcard) { + if (profileWindow) { + profileWindow->setVCard(vcard); + gettingVCard = false; + updateDialogStatus(); + } +} + +void ProfileController::setAvailable(bool b) { + available = b; + updateDialogStatus(); +} + + +void ProfileController::updateDialogStatus() { + if (profileWindow) { + profileWindow->setEnabled(available && !gettingVCard && !pendingSetVCardRequest); + profileWindow->setProcessing(gettingVCard || pendingSetVCardRequest); + } +} + +} diff --git a/Swift/Controllers/ProfileController.h b/Swift/Controllers/ProfileController.h new file mode 100644 index 0000000..c1afcf9 --- /dev/null +++ b/Swift/Controllers/ProfileController.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Swift { + class UIEventStream; + class ProfileWindowFactory; + class ProfileWindow; + class VCardManager; + + class ProfileController { + public: + ProfileController(VCardManager* vcardManager, ProfileWindowFactory* profileWindowFactory, UIEventStream* uiEventStream); + ~ProfileController(); + + void setAvailable(bool b); + + private: + void handleUIEvent(UIEvent::ref event); + void handleVCardChangeRequest(VCard::ref vcard); + void handleSetVCardResponse(ErrorPayload::ref); + void handleOwnVCardChanged(VCard::ref vcard); + void updateDialogStatus(); + + private: + VCardManager* vcardManager; + ProfileWindowFactory* profileWindowFactory; + UIEventStream* uiEventStream; + bool available; + SetVCardRequest::ref pendingSetVCardRequest; + ProfileWindow* profileWindow; + bool gettingVCard; + }; +} + diff --git a/Swift/Controllers/RosterController.cpp b/Swift/Controllers/RosterController.cpp index 282f041..0f149f6 100644 --- a/Swift/Controllers/RosterController.cpp +++ b/Swift/Controllers/RosterController.cpp @@ -7,6 +7,7 @@ #include "Swift/Controllers/RosterController.h" #include +#include #include "Swiften/Base/foreach.h" #include "Swift/Controllers/UIInterfaces/MainWindow.h" @@ -32,6 +33,7 @@ #include "Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h" #include "Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h" #include "Swift/Controllers/UIEvents/ToggleShowOfflineUIEvent.h" +#include "Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h" #include namespace Swift { @@ -42,7 +44,7 @@ static const String SHOW_OFFLINE = "showOffline"; * The controller does not gain ownership of these parameters. */ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, AvatarManager* avatarManager, MainWindowFactory* mainWindowFactory, NickManager* nickManager, NickResolver* nickResolver, PresenceOracle* presenceOracle, SubscriptionManager* subscriptionManager, EventController* eventController, UIEventStream* uiEventStream, IQRouter* iqRouter, SettingsProvider* settings) - : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), nickManager_(nickManager), nickResolver_(nickResolver) { + : myJID_(jid), xmppRoster_(xmppRoster), mainWindowFactory_(mainWindowFactory), mainWindow_(mainWindowFactory_->createMainWindow(uiEventStream)), roster_(new Roster()), offlineFilter_(new OfflineRosterFilter()), nickManager_(nickManager), nickResolver_(nickResolver), uiEventStream_(uiEventStream) { iqRouter_ = iqRouter; presenceOracle_ = presenceOracle; subscriptionManager_ = subscriptionManager; @@ -54,6 +56,7 @@ RosterController::RosterController(const JID& jid, XMPPRoster* xmppRoster, Avata changeStatusConnection_ = mainWindow_->onChangeStatusRequest.connect(boost::bind(&RosterController::handleChangeStatusRequest, this, _1, _2)); signOutConnection_ = mainWindow_->onSignOutRequest.connect(boost::bind(boost::ref(onSignOutRequest))); + mainWindow_->onEditProfileRequest.connect(boost::bind(&RosterController::handleEditProfileRequest, this)); xmppRoster_->onJIDAdded.connect(boost::bind(&RosterController::handleOnJIDAdded, this, _1)); xmppRoster_->onJIDUpdated.connect(boost::bind(&RosterController::handleOnJIDUpdated, this, _1, _2, _3)); xmppRoster_->onJIDRemoved.connect(boost::bind(&RosterController::handleOnJIDRemoved, this, _1)); @@ -79,6 +82,8 @@ RosterController::~RosterController() { delete offlineFilter_; delete expandiness_; + + mainWindow_->onEditProfileRequest.disconnect(boost::bind(&RosterController::handleEditProfileRequest, this)); mainWindow_->setRosterModel(NULL); if (mainWindow_->canDelete()) { delete mainWindow_; @@ -280,4 +285,8 @@ void RosterController::handleAvatarChanged(const JID& jid) { } } +void RosterController::handleEditProfileRequest() { + uiEventStream_->onUIEvent(boost::make_shared()); +} + } diff --git a/Swift/Controllers/RosterController.h b/Swift/Controllers/RosterController.h index 79c14b9..9e22686 100644 --- a/Swift/Controllers/RosterController.h +++ b/Swift/Controllers/RosterController.h @@ -60,6 +60,8 @@ namespace Swift { void handleUIEvent(boost::shared_ptr event); void handleRosterSetError(ErrorPayload::ref error, boost::shared_ptr rosterPayload); void applyAllPresenceTo(const JID& jid); + void handleEditProfileRequest(); + JID myJID_; XMPPRoster* xmppRoster_; MainWindowFactory* mainWindowFactory_; @@ -75,6 +77,7 @@ namespace Swift { RosterGroupExpandinessPersister* expandiness_; IQRouter* iqRouter_; SettingsProvider* settings_; + UIEventStream* uiEventStream_; boost::bsignals::scoped_connection changeStatusConnection_; boost::bsignals::scoped_connection signOutConnection_; boost::bsignals::scoped_connection uiEventConnection_; diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 96674bd..b149cd2 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -29,6 +29,7 @@ if env["SCONS_STAGE"] == "build" : "Chat/UserSearchController.cpp", "DiscoServiceWalker.cpp", "MainController.cpp", + "ProfileController.cpp", "RosterController.cpp", "RosterGroupExpandinessPersister.cpp", "EventWindowController.cpp", diff --git a/Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h b/Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h new file mode 100644 index 0000000..107e2e8 --- /dev/null +++ b/Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include "Swift/Controllers/UIEvents/UIEvent.h" + +namespace Swift { + class RequestProfileEditorUIEvent : public UIEvent { + public: + RequestProfileEditorUIEvent() {} + }; +} diff --git a/Swift/Controllers/UIEvents/UIEvent.h b/Swift/Controllers/UIEvents/UIEvent.h index ab57634..e79a4f1 100644 --- a/Swift/Controllers/UIEvents/UIEvent.h +++ b/Swift/Controllers/UIEvents/UIEvent.h @@ -6,9 +6,13 @@ #pragma once +#include + namespace Swift { class UIEvent { public: + typedef boost::shared_ptr ref; + virtual ~UIEvent(); }; } diff --git a/Swift/Controllers/UIInterfaces/MainWindow.h b/Swift/Controllers/UIInterfaces/MainWindow.h index 125aae5..55087fe 100644 --- a/Swift/Controllers/UIInterfaces/MainWindow.h +++ b/Swift/Controllers/UIInterfaces/MainWindow.h @@ -36,6 +36,7 @@ namespace Swift { boost::signal onChangeStatusRequest; boost::signal onSignOutRequest; + boost::signal onEditProfileRequest; private: bool canDelete_; diff --git a/Swift/Controllers/UIInterfaces/ProfileWindow.h b/Swift/Controllers/UIInterfaces/ProfileWindow.h new file mode 100644 index 0000000..e9c9a63 --- /dev/null +++ b/Swift/Controllers/UIInterfaces/ProfileWindow.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +#include + +namespace Swift { + class ProfileWindow { + public: + virtual ~ProfileWindow() {}; + + virtual void setVCard(VCard::ref vcard) = 0; + + virtual void setEnabled(bool b) = 0; + virtual void setProcessing(bool b) = 0; + virtual void setError(const String&) = 0; + + virtual void show() = 0; + virtual void hide() = 0; + + boost::signal onVCardChangeRequest; + }; +} diff --git a/Swift/Controllers/UIInterfaces/ProfileWindowFactory.h b/Swift/Controllers/UIInterfaces/ProfileWindowFactory.h new file mode 100644 index 0000000..022c3eb --- /dev/null +++ b/Swift/Controllers/UIInterfaces/ProfileWindowFactory.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include + +namespace Swift { + class ProfileWindowFactory { + public: + virtual ~ProfileWindowFactory() {}; + + virtual ProfileWindow* createProfileWindow() = 0; + }; +} diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h index 4783dc8..11623d7 100644 --- a/Swift/Controllers/UIInterfaces/UIFactory.h +++ b/Swift/Controllers/UIInterfaces/UIFactory.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace Swift { class UIFactory : @@ -26,7 +27,8 @@ namespace Swift { public MUCSearchWindowFactory, public XMLConsoleWidgetFactory, public UserSearchWindowFactory, - public JoinMUCWindowFactory { + public JoinMUCWindowFactory, + public ProfileWindowFactory { public: virtual ~UIFactory() {} }; diff --git a/Swift/QtUI/QtAvatarWidget.cpp b/Swift/QtUI/QtAvatarWidget.cpp new file mode 100644 index 0000000..1ee7c73 --- /dev/null +++ b/Swift/QtUI/QtAvatarWidget.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2011 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "QtAvatarWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Swift { + +QtAvatarWidget::QtAvatarWidget(QWidget* parent) : QWidget(parent) { + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(0,0,0,0); + + QSizePolicy sp(QSizePolicy::Fixed, QSizePolicy::Fixed); + sp.setHorizontalStretch(0); + sp.setVerticalStretch(0); + setSizePolicy(sp); + setMinimumSize(QSize(96, 96)); + setMaximumSize(QSize(96, 96)); + + label = new QLabel(this); + label->setWordWrap(true); + label->setSizePolicy(sp); + label->setMinimumSize(QSize(96, 96)); + label->setMaximumSize(QSize(96, 96)); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); +} + +void QtAvatarWidget::setAvatar(const ByteArray& data, const String& type) { + this->data = data; + this->type = type; + + QImage image; + if (!data.isEmpty()) { + image.loadFromData(reinterpret_cast(data.getData()), data.getSize()); + } + + if (image.isNull()) { + image = QImage(":/icons/no-avatar.png"); + QPainter painter(&image); + painter.setPen(Qt::gray); + painter.drawText(0, 0, image.height(), image.width(), Qt::AlignHCenter | Qt::AlignVCenter, "No picture"); + } + + if (image.height() > label->height() || image.width() > label->width()) { + image = image.scaled(label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + label->setPixmap(QPixmap::fromImage(image)); +} + +void QtAvatarWidget::mousePressEvent(QMouseEvent* event) { + QMenu menu; + + QAction* selectPicture = new QAction("Select picture ...", this); + menu.addAction(selectPicture); + + QAction* clearPicture = new QAction("Clear picture", this); + menu.addAction(clearPicture); + + QAction* result = menu.exec(event->globalPos()); + if (result == selectPicture) { + QString fileName = QFileDialog::getOpenFileName(this, tr("Select picture"), "", tr("Image Files (*.png *.jpg *.gif)")); + if (!fileName.isEmpty()) { + ByteArray data; + data.readFromFile(Q2PSTRING(fileName)); + + QBuffer buffer; + buffer.setData(data.getData(), data.getSize()); + buffer.open(QIODevice::ReadOnly); + QString type = QImageReader::imageFormat(&buffer).toLower(); + if (!type.isEmpty()) { + type = "image/" + type; + setAvatar(data, Q2PSTRING(type)); + } + else { + QMessageBox::critical(this, "Error", "The selected picture is in an unrecognized format"); + } + } + } + else if (result == clearPicture) { + setAvatar(ByteArray(), ""); + } +} + + + +} diff --git a/Swift/QtUI/QtAvatarWidget.h b/Swift/QtUI/QtAvatarWidget.h new file mode 100644 index 0000000..ce4d192 --- /dev/null +++ b/Swift/QtUI/QtAvatarWidget.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2011 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include +#include + +class QLabel; + +namespace Swift { + class QtAvatarWidget : public QWidget { + Q_OBJECT + public: + QtAvatarWidget(QWidget* parent); + + void setAvatar(const ByteArray& data, const String& type); + + const ByteArray& getAvatarData() const { + return data; + } + + const String& getAvatarType() const { + return type; + } + + void mousePressEvent(QMouseEvent* event); + + private: + ByteArray data; + String type; + QLabel* label; + }; +} diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index 0411c0b..d313aba 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -83,6 +83,9 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS QMenu* actionsMenu = new QMenu(tr("&Actions"), this); menus_.push_back(actionsMenu); + QAction* editProfileAction = new QAction("Edit Profile", this); + connect(editProfileAction, SIGNAL(triggered()), SLOT(handleEditProfileAction())); + actionsMenu->addAction(editProfileAction); QAction* joinMUCAction = new QAction("&Join Room", this); connect(joinMUCAction, SIGNAL(triggered()), SLOT(handleJoinMUCAction())); actionsMenu->addAction(joinMUCAction); @@ -143,6 +146,10 @@ void QtMainWindow::handleSignOutAction() { onSignOutRequest(); } +void QtMainWindow::handleEditProfileAction() { + onEditProfileRequest(); +} + void QtMainWindow::handleJoinMUCAction() { uiEventStream_->send(boost::make_shared()); } @@ -190,5 +197,6 @@ void QtMainWindow::setConnecting() { meView_->setConnecting(); } + } diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h index 27972cb..938feff 100644 --- a/Swift/QtUI/QtMainWindow.h +++ b/Swift/QtUI/QtMainWindow.h @@ -54,6 +54,7 @@ namespace Swift { void handleShowOfflineToggled(bool); void handleJoinMUCAction(); void handleSignOutAction(); + void handleEditProfileAction(); void handleAddUserActionTriggered(bool checked); void handleChatUserActionTriggered(bool checked); void handleEventCountUpdated(int count); diff --git a/Swift/QtUI/QtProfileWindow.cpp b/Swift/QtUI/QtProfileWindow.cpp new file mode 100644 index 0000000..0a53f11 --- /dev/null +++ b/Swift/QtUI/QtProfileWindow.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2011 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "QtProfileWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QtSwiftUtil.h" +#include "QtAvatarWidget.h" + +namespace Swift { + +QtProfileWindow::QtProfileWindow() { + setWindowTitle("Edit Profile"); + + QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(0); + sizePolicy.setHeightForWidth(this->sizePolicy().hasHeightForWidth()); + setSizePolicy(sizePolicy); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(10, 10, 10, 10); + + QHBoxLayout* topLayout = new QHBoxLayout(); + + avatar = new QtAvatarWidget(this); + topLayout->addWidget(avatar); + + QVBoxLayout* fieldsLayout = new QVBoxLayout(); + + QHBoxLayout* horizontalLayout_2 = new QHBoxLayout(); + nicknameLabel = new QLabel("Nickname: ", this); + horizontalLayout_2->addWidget(nicknameLabel); + nickname = new QLineEdit(this); + horizontalLayout_2->addWidget(nickname); + + fieldsLayout->addLayout(horizontalLayout_2); + + errorLabel = new QLabel(this); + errorLabel->setAlignment(Qt::AlignHCenter); + fieldsLayout->addWidget(errorLabel); + + fieldsLayout->addItem(new QSpacerItem(198, 17, QSizePolicy::Minimum, QSizePolicy::Expanding)); + topLayout->addLayout(fieldsLayout); + + layout->addLayout(topLayout); + + QHBoxLayout* horizontalLayout = new QHBoxLayout(); + horizontalLayout->setContentsMargins(0, 0, 0, 0); + horizontalLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); + + throbberLabel = new QLabel(this); + throbberLabel->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), this)); + horizontalLayout->addWidget(throbberLabel); + + saveButton = new QPushButton("Save", this); + connect(saveButton, SIGNAL(clicked()), SLOT(handleSave())); + horizontalLayout->addWidget(saveButton); + + fieldsLayout->addLayout(horizontalLayout); + + resize(360, 120); +} + +void QtProfileWindow::setVCard(Swift::VCard::ref vcard) { + this->vcard = vcard; + nickname->setText(P2QSTRING(vcard->getNickname())); + avatar->setAvatar(vcard->getPhoto(), vcard->getPhotoType()); +} + +void QtProfileWindow::setEnabled(bool b) { + nickname->setEnabled(b); + nicknameLabel->setEnabled(b); + avatar->setEnabled(b); + saveButton->setEnabled(b); +} + +void QtProfileWindow::setProcessing(bool processing) { + if (processing) { + throbberLabel->movie()->start(); + throbberLabel->show(); + } + else { + throbberLabel->hide(); + throbberLabel->movie()->stop(); + } +} + +void QtProfileWindow::show() { + QWidget::show(); + QWidget::activateWindow(); +} + +void QtProfileWindow::hideEvent(QHideEvent* event) { + QWidget::hideEvent(event); +} + +void QtProfileWindow::hide() { + QWidget::hide(); +} + +void QtProfileWindow::handleSave() { + assert(vcard); + vcard->setNickname(Q2PSTRING(nickname->text())); + vcard->setPhoto(avatar->getAvatarData()); + vcard->setPhotoType(avatar->getAvatarType()); + onVCardChangeRequest(vcard); +} + +void QtProfileWindow::setError(const String& error) { + if (!error.isEmpty()) { + errorLabel->setText("" + P2QSTRING(error) + ""); + } + else { + errorLabel->setText(""); + } +} + + + +} diff --git a/Swift/QtUI/QtProfileWindow.h b/Swift/QtUI/QtProfileWindow.h new file mode 100644 index 0000000..1ad73e8 --- /dev/null +++ b/Swift/QtUI/QtProfileWindow.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2011 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include + +#include + +class QLabel; +class QLineEdit; +class QHBoxLayout; +class QPushButton; + +namespace Swift { + class QtAvatarWidget; + + class QtProfileWindow : public QWidget, public ProfileWindow { + Q_OBJECT + public: + QtProfileWindow(); + + void setVCard(Swift::VCard::ref); + void setEnabled(bool); + void setProcessing(bool); + virtual void setError(const String&); + void show(); + void hide(); + + void hideEvent (QHideEvent* event); + + private slots: + void handleSave(); + + private: + VCard::ref vcard; + QtAvatarWidget* avatar; + QLabel* nicknameLabel; + QLineEdit* nickname; + QLabel* throbberLabel; + QLabel* errorLabel; + QHBoxLayout* horizontalLayout; + QPushButton* saveButton; + }; +} diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 8cb9863..953d658 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -21,6 +21,7 @@ #include "QtSwiftUtil.h" #include "MUCSearch/QtMUCSearchWindow.h" #include "UserSearch/QtUserSearchWindow.h" +#include "QtProfileWindow.h" namespace Swift { @@ -87,4 +88,8 @@ JoinMUCWindow* QtUIFactory::createJoinMUCWindow() { return new QtJoinMUCWindow(); } +ProfileWindow* QtUIFactory::createProfileWindow() { + return new QtProfileWindow(); +} + } diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h index 4d80338..199cebf 100644 --- a/Swift/QtUI/QtUIFactory.h +++ b/Swift/QtUI/QtUIFactory.h @@ -35,6 +35,7 @@ namespace Swift { virtual ChatWindow* createChatWindow(const JID &contact, UIEventStream* eventStream); virtual UserSearchWindow* createUserSearchWindow(UserSearchWindow::Type type, UIEventStream* eventStream); virtual JoinMUCWindow* createJoinMUCWindow(); + virtual ProfileWindow* createProfileWindow(); private slots: void handleLoginWindowGeometryChanged(); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index b0072a6..05555f6 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -63,11 +63,13 @@ sources = [ "main.cpp", "QtAboutWidget.cpp", "QtAddContactDialog.cpp", + "QtAvatarWidget.cpp", "QtUIFactory.cpp", "QtChatWindowFactory.cpp", "QtChatWindow.cpp", "QtLoginWindow.cpp", "QtMainWindow.cpp", + "QtProfileWindow.cpp", "QtNameWidget.cpp", "QtSettingsProvider.cpp", "QtStatusWidget.cpp", diff --git a/Swift/QtUI/Swift.qrc b/Swift/QtUI/Swift.qrc index 9a80339..2f8d494 100644 --- a/Swift/QtUI/Swift.qrc +++ b/Swift/QtUI/Swift.qrc @@ -13,6 +13,7 @@ ../resources/icons/error.png ../resources/icons/throbber.gif ../resources/icons/avatar.png + ../resources/icons/no-avatar.png ../resources/icons/tray-standard.png ../resources/icons/new-chat.png ../../COPYING diff --git a/Swift/resources/icons/no-avatar.png b/Swift/resources/icons/no-avatar.png new file mode 100644 index 0000000..c6dc381 Binary files /dev/null and b/Swift/resources/icons/no-avatar.png differ diff --git a/Swiften/VCards/SetVCardRequest.h b/Swiften/VCards/SetVCardRequest.h new file mode 100644 index 0000000..8dfda5d --- /dev/null +++ b/Swiften/VCards/SetVCardRequest.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + + +namespace Swift { + class SetVCardRequest : public GenericRequest { + public: + typedef boost::shared_ptr ref; + + static ref create(VCard::ref vcard, IQRouter* router) { + return ref(new SetVCardRequest(vcard, router)); + } + + private: + SetVCardRequest(VCard::ref vcard, IQRouter* router) : GenericRequest(IQ::Set, JID(), vcard, router) { + } + }; +} diff --git a/Swiften/VCards/UnitTest/VCardManagerTest.cpp b/Swiften/VCards/UnitTest/VCardManagerTest.cpp index 56bbfa1..1f81f8e 100644 --- a/Swiften/VCards/UnitTest/VCardManagerTest.cpp +++ b/Swiften/VCards/UnitTest/VCardManagerTest.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "Swiften/VCards/VCardManager.h" #include "Swiften/VCards/VCardMemoryStorage.h" @@ -28,6 +29,8 @@ class VCardManagerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testRequest_VCardAlreadyRequested); CPPUNIT_TEST(testRequest_AfterPreviousRequest); CPPUNIT_TEST(testRequestOwnVCard); + CPPUNIT_TEST(testCreateSetVCardRequest); + CPPUNIT_TEST(testCreateSetVCardRequest_Error); CPPUNIT_TEST_SUITE_END(); public: @@ -82,6 +85,8 @@ class VCardManagerTest : public CppUnit::TestFixture { CPPUNIT_ASSERT_EQUAL(JID("foo@bar.com/baz"), changes[0].first); CPPUNIT_ASSERT_EQUAL(String("Foo Bar"), changes[0].second->getFullName()); CPPUNIT_ASSERT_EQUAL(String("Foo Bar"), vcardStorage->getVCard(JID("foo@bar.com/baz"))->getFullName()); + + CPPUNIT_ASSERT_EQUAL(0, static_cast(ownChanges.size())); } void testRequest_Error() { @@ -125,12 +130,42 @@ class VCardManagerTest : public CppUnit::TestFixture { CPPUNIT_ASSERT_EQUAL(ownJID.toBare(), changes[0].first); CPPUNIT_ASSERT_EQUAL(String("Myself"), changes[0].second->getFullName()); CPPUNIT_ASSERT_EQUAL(String("Myself"), vcardStorage->getVCard(ownJID.toBare())->getFullName()); + + CPPUNIT_ASSERT_EQUAL(1, static_cast(ownChanges.size())); + CPPUNIT_ASSERT_EQUAL(String("Myself"), ownChanges[0]->getFullName()); + } + + void testCreateSetVCardRequest() { + std::auto_ptr testling = createManager(); + VCard::ref vcard = boost::make_shared(); + vcard->setFullName("New Name"); + SetVCardRequest::ref request = testling->createSetVCardRequest(vcard); + request->send(); + + stanzaChannel->onIQReceived(createSetVCardResult()); + + CPPUNIT_ASSERT_EQUAL(1, static_cast(changes.size())); + CPPUNIT_ASSERT_EQUAL(ownJID.toBare(), changes[0].first); + CPPUNIT_ASSERT_EQUAL(String("New Name"), changes[0].second->getFullName()); + } + + void testCreateSetVCardRequest_Error() { + std::auto_ptr testling = createManager(); + VCard::ref vcard = boost::make_shared(); + vcard->setFullName("New Name"); + SetVCardRequest::ref request = testling->createSetVCardRequest(vcard); + request->send(); + + stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID())); + + CPPUNIT_ASSERT_EQUAL(0, static_cast(changes.size())); } private: std::auto_ptr createManager() { std::auto_ptr manager(new VCardManager(ownJID, iqRouter, vcardStorage)); manager->onVCardChanged.connect(boost::bind(&VCardManagerTest::handleVCardChanged, this, _1, _2)); + manager->onOwnVCardChanged.connect(boost::bind(&VCardManagerTest::handleOwnVCardChanged, this, _1)); return manager; } @@ -138,6 +173,10 @@ class VCardManagerTest : public CppUnit::TestFixture { changes.push_back(std::pair(jid, vcard)); } + void handleOwnVCardChanged(VCard::ref vcard) { + ownChanges.push_back(vcard); + } + IQ::ref createVCardResult() { VCard::ref vcard(new VCard()); vcard->setFullName("Foo Bar"); @@ -150,12 +189,18 @@ class VCardManagerTest : public CppUnit::TestFixture { return IQ::createResult(JID(), stanzaChannel->sentStanzas[0]->getID(), vcard); } + IQ::ref createSetVCardResult() { + return IQ::createResult(JID("baz@fum.com/dum"), stanzaChannel->sentStanzas[0]->getID(), VCard::ref()); + } + + private: JID ownJID; DummyStanzaChannel* stanzaChannel; IQRouter* iqRouter; VCardMemoryStorage* vcardStorage; std::vector< std::pair > changes; + std::vector ownChanges; }; CPPUNIT_TEST_SUITE_REGISTRATION(VCardManagerTest); diff --git a/Swiften/VCards/VCardManager.cpp b/Swiften/VCards/VCardManager.cpp index 8d695d4..de53238 100644 --- a/Swiften/VCards/VCardManager.cpp +++ b/Swiften/VCards/VCardManager.cpp @@ -17,6 +17,9 @@ namespace Swift { VCardManager::VCardManager(const JID& ownJID, IQRouter* iqRouter, VCardStorage* vcardStorage) : ownJID(ownJID), iqRouter(iqRouter), storage(vcardStorage) { } +VCardManager::~VCardManager() { +} + VCard::ref VCardManager::getVCard(const JID& jid) const { return storage->getVCard(jid); } @@ -51,8 +54,27 @@ void VCardManager::handleVCardReceived(const JID& actualJID, VCard::ref vcard, E } requestedVCards.erase(actualJID); JID jid = actualJID.isValid() ? actualJID : ownJID.toBare(); + setVCard(jid, vcard); +} + +SetVCardRequest::ref VCardManager::createSetVCardRequest(VCard::ref vcard) { + SetVCardRequest::ref request = SetVCardRequest::create(vcard, iqRouter); + request->onResponse.connect(boost::bind(&VCardManager::handleSetVCardResponse, this, vcard, _2)); + return request; +} + +void VCardManager::handleSetVCardResponse(VCard::ref vcard, ErrorPayload::ref error) { + if (!error) { + setVCard(ownJID.toBare(), vcard); + } +} + +void VCardManager::setVCard(const JID& jid, VCard::ref vcard) { storage->setVCard(jid, vcard); onVCardChanged(jid, vcard); + if (jid.compare(ownJID, JID::WithoutResource) == 0) { + onOwnVCardChanged(vcard); + } } } diff --git a/Swiften/VCards/VCardManager.h b/Swiften/VCards/VCardManager.h index e1ed44a..5cdf82e 100644 --- a/Swiften/VCards/VCardManager.h +++ b/Swiften/VCards/VCardManager.h @@ -6,35 +6,48 @@ #pragma once -#include #include #include "Swiften/JID/JID.h" #include "Swiften/Elements/VCard.h" #include "Swiften/Elements/ErrorPayload.h" +#include +#include namespace Swift { class JID; class VCardStorage; class IQRouter; - class VCardManager { + class VCardManager : public boost::bsignals::trackable { public: VCardManager(const JID& ownJID, IQRouter* iqRouter, VCardStorage* vcardStorage); + ~VCardManager(); VCard::ref getVCard(const JID& jid) const; VCard::ref getVCardAndRequestWhenNeeded(const JID& jid); void requestVCard(const JID& jid); void requestOwnVCard(); + SetVCardRequest::ref createSetVCardRequest(VCard::ref); + public: /** * The JID will always be bare. */ boost::signal onVCardChanged; + /** + * Emitted when our own vcard changes. + * + * onVCardChanged will also be emitted. + */ + boost::signal onOwnVCardChanged; + private: void handleVCardReceived(const JID& from, VCard::ref, ErrorPayload::ref); + void handleSetVCardResponse(VCard::ref, ErrorPayload::ref); + void setVCard(const JID& jid, VCard::ref vcard); private: JID ownJID; -- cgit v0.10.2-6-g49f6