From b897bac235a95f9c4654b31d101779bd0cc8f72f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Remko=20Tron=C3=A7on?= <git@el-tramo.be>
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 <Swift/Controllers/ProfileController.h>
 
 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<DiscoInfo> 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 <Swift/Controllers/ProfileController.h>
+
+#include <boost/bind.hpp>
+
+#include <Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h>
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIInterfaces/ProfileWindowFactory.h>
+#include <Swiften/VCards/VCardManager.h>
+
+
+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<RequestProfileEditorUIEvent>(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 <Swift/Controllers/UIEvents/UIEvent.h>
+#include <Swiften/Elements/VCard.h>
+#include <Swiften/Elements/ErrorPayload.h>
+#include <Swiften/VCards/SetVCardRequest.h>
+
+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 <boost/bind.hpp>
+#include <boost/smart_ptr/make_shared.hpp>
 
 #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 <Swiften/Client/NickManager.h>
 
 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<RequestProfileEditorUIEvent>());
+}
+
 }
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<UIEvent> event);
 			void handleRosterSetError(ErrorPayload::ref error, boost::shared_ptr<RosterPayload> 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 <boost/shared_ptr.hpp>
+
 namespace Swift {
 	class UIEvent {
 		public:
+			typedef boost::shared_ptr<UIEvent> 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<void (StatusShow::Type, const String&)> onChangeStatusRequest;
 			boost::signal<void ()> onSignOutRequest;
+			boost::signal<void ()> 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 <Swiften/Base/boost_bsignals.h>
+#include <boost/shared_ptr.hpp>
+
+#include <Swiften/Elements/VCard.h>
+
+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<void (VCard::ref)> 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 <Swift/Controllers/UIInterfaces/ProfileWindow.h>
+
+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 <Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/XMLConsoleWidgetFactory.h>
+#include <Swift/Controllers/UIInterfaces/ProfileWindowFactory.h>
 
 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 <QLabel>
+#include <QVBoxLayout>
+#include <QPixmap>
+#include <QMenu>
+#include <QAction>
+#include <QMouseEvent>
+#include <QFileDialog>
+#include <QImageReader>
+#include <QBuffer>
+#include <QMessageBox>
+#include <QPainter>
+
+#include <QtSwiftUtil.h>
+
+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<const uchar*>(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 <QWidget>
+#include <QImage>
+#include <Swiften/Base/ByteArray.h>
+
+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<RequestJoinMUCUIEvent>());
 }
@@ -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 <QImage>
+#include <QPixmap>
+#include <QSizePolicy>
+#include <QGridLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QMovie>
+
+#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("<font color='red'>" + P2QSTRING(error) + "</font>");
+	}
+	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 <QWidget>
+
+#include <Swift/Controllers/UIInterfaces/ProfileWindow.h>
+
+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 @@
 		<file alias="icons/error.png">../resources/icons/error.png</file>
 		<file alias="icons/throbber.gif">../resources/icons/throbber.gif</file>
 		<file alias="icons/avatar.png">../resources/icons/avatar.png</file>
+		<file alias="icons/no-avatar.png">../resources/icons/no-avatar.png</file>
 		<file alias="icons/tray-standard.png">../resources/icons/tray-standard.png</file>
 		<file alias="icons/new-chat.png">../resources/icons/new-chat.png</file>
 		<file alias="COPYING">../../COPYING</file>
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 <Swiften/Queries/GenericRequest.h>
+#include <Swiften/Elements/VCard.h>
+
+
+namespace Swift {
+	class SetVCardRequest : public GenericRequest<VCard> {
+		public:
+			typedef boost::shared_ptr<SetVCardRequest> ref;
+
+			static ref create(VCard::ref vcard, IQRouter* router) {
+				return ref(new SetVCardRequest(vcard, router));
+			}
+
+		private:
+			SetVCardRequest(VCard::ref vcard, IQRouter* router) : GenericRequest<VCard>(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 <cppunit/extensions/TestFactoryRegistry.h>
 #include <vector>
 #include <boost/bind.hpp>
+#include <boost/smart_ptr/make_shared.hpp>
 
 #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<int>(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<int>(ownChanges.size()));
+			CPPUNIT_ASSERT_EQUAL(String("Myself"), ownChanges[0]->getFullName());
+		}
+
+		void testCreateSetVCardRequest() {
+			std::auto_ptr<VCardManager> testling = createManager();
+			VCard::ref vcard = boost::make_shared<VCard>();
+			vcard->setFullName("New Name");
+			SetVCardRequest::ref request = testling->createSetVCardRequest(vcard);
+			request->send();
+
+			stanzaChannel->onIQReceived(createSetVCardResult());
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(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<VCardManager> testling = createManager();
+			VCard::ref vcard = boost::make_shared<VCard>();
+			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<int>(changes.size()));
 		}
 
 	private:
 		std::auto_ptr<VCardManager> createManager() {
 			std::auto_ptr<VCardManager> 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::ref>(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<JID, VCard::ref> > changes;
+		std::vector<VCard::ref> 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 <boost/signals.hpp>
 #include <set>
 
 #include "Swiften/JID/JID.h"
 #include "Swiften/Elements/VCard.h"
 #include "Swiften/Elements/ErrorPayload.h"
+#include <Swiften/VCards/SetVCardRequest.h>
+#include <Swiften/Base/boost_bsignals.h>
 
 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<void (const JID&, VCard::ref)> onVCardChanged;
 
+			/**
+			 * Emitted when our own vcard changes.
+			 *
+			 * onVCardChanged will also be emitted.
+			 */
+			boost::signal<void (VCard::ref)> 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