From afcfa9dd33cfb5e36edf7d8148a7f8b24c976741 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Remko=20Tron=C3=A7on?= <git@el-tramo.be>
Date: Sun, 6 Feb 2011 23:50:30 +0100
Subject: Reworking contact editing.

Collapsed rename, group edit, and remove into one dialog.
Moved contact editing logic to controllers.

diff --git a/Swift/Controllers/ContactEditController.cpp b/Swift/Controllers/ContactEditController.cpp
new file mode 100644
index 0000000..286fdeb
--- /dev/null
+++ b/Swift/Controllers/ContactEditController.cpp
@@ -0,0 +1,76 @@
+/*
+ * 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/ContactEditController.h>
+
+#include <boost/bind.hpp>
+#include <boost/smart_ptr/make_shared.hpp>
+
+#include <Swift/Controllers/UIEvents/RequestContactEditorUIEvent.h>
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIEvents/RemoveRosterItemUIEvent.h>
+#include <Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h>
+#include <Swift/Controllers/RosterController.h>
+
+
+namespace Swift {
+
+ContactEditController::ContactEditController(RosterController* rosterController, ContactEditWindowFactory* contactEditWindowFactory, UIEventStream* uiEventStream) : rosterController(rosterController), contactEditWindowFactory(contactEditWindowFactory), uiEventStream(uiEventStream), contactEditWindow(NULL) {
+	uiEventStream->onUIEvent.connect(boost::bind(&ContactEditController::handleUIEvent, this, _1));
+}
+
+ContactEditController::~ContactEditController() {
+	if (contactEditWindow) {
+		contactEditWindow->onChangeContactRequest.disconnect(boost::bind(&ContactEditController::handleChangeContactRequest, this, _1, _2));
+		contactEditWindow->onRemoveContactRequest.disconnect(boost::bind(&ContactEditController::handleRemoveContactRequest, this));
+		delete contactEditWindow;
+	}
+	uiEventStream->onUIEvent.disconnect(boost::bind(&ContactEditController::handleUIEvent, this, _1));
+}
+
+void ContactEditController::handleUIEvent(UIEvent::ref event) {
+	RequestContactEditorUIEvent::ref editEvent = boost::dynamic_pointer_cast<RequestContactEditorUIEvent>(event);
+	if (!editEvent) {
+		return;
+	}
+
+	if (!contactEditWindow) {
+		contactEditWindow = contactEditWindowFactory->createContactEditWindow();
+		contactEditWindow->onRemoveContactRequest.connect(boost::bind(&ContactEditController::handleRemoveContactRequest, this));
+		contactEditWindow->onChangeContactRequest.connect(boost::bind(&ContactEditController::handleChangeContactRequest, this, _1, _2));
+	}
+	currentContact = rosterController->getItem(editEvent->getJID());
+	assert(currentContact);
+	contactEditWindow->setContact(currentContact->getJID(), currentContact->getName(), currentContact->getGroups(), rosterController->getGroups());
+	contactEditWindow->show();
+}
+
+void ContactEditController::setAvailable(bool b) {
+	if (contactEditWindow) {
+		contactEditWindow->setEnabled(b);
+	}
+}
+
+void ContactEditController::handleRemoveContactRequest() {
+	assert(currentContact);
+	uiEventStream->send(boost::make_shared<RemoveRosterItemUIEvent>(currentContact->getJID()));
+	contactEditWindow->hide();
+}
+
+void ContactEditController::handleChangeContactRequest(const String& name, const std::vector<String>& groups) {
+	std::vector<String> oldGroupsVector = currentContact->getGroups();
+	std::set<String> oldGroups(oldGroupsVector.begin(), oldGroupsVector.end());
+	std::set<String> newGroups(groups.begin(), groups.end());
+	if (oldGroups != newGroups || currentContact->getName() != name) {
+		XMPPRosterItem newContact(*currentContact);
+		newContact.setName(name);
+		newContact.setGroups(groups);
+		rosterController->updateItem(newContact);
+	}
+	contactEditWindow->hide();
+}
+
+}
diff --git a/Swift/Controllers/ContactEditController.h b/Swift/Controllers/ContactEditController.h
new file mode 100644
index 0000000..31917b1
--- /dev/null
+++ b/Swift/Controllers/ContactEditController.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <vector>
+#include <boost/optional.hpp>
+
+#include <Swiften/JID/JID.h>
+#include <Swiften/Base/String.h>
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+#include <Swiften/Roster/XMPPRosterItem.h>
+
+namespace Swift {
+	class UIEventStream;
+	class ContactEditWindowFactory;
+	class ContactEditWindow;
+	class RosterController;
+
+	class ContactEditController {
+		public:
+			ContactEditController(RosterController* rosterController, ContactEditWindowFactory* contactEditWindowFactory, UIEventStream* uiEventStream);
+			~ContactEditController();
+
+			void setAvailable(bool b);
+
+		private:
+			void handleRemoveContactRequest();
+			void handleChangeContactRequest(const String& name, const std::vector<String>& groups);
+
+		private:
+			void handleUIEvent(UIEvent::ref event);
+
+		private:
+			boost::optional<XMPPRosterItem> currentContact;
+			RosterController* rosterController;
+			ContactEditWindowFactory* contactEditWindowFactory;
+			UIEventStream* uiEventStream;
+			ContactEditWindow* contactEditWindow;
+	};
+}
+
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 1b513cd..3e1b366 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -62,6 +62,7 @@
 #include "Swift/Controllers/CertificateStorageTrustChecker.h"
 #include "Swiften/Network/NetworkFactories.h"
 #include <Swift/Controllers/ProfileController.h>
+#include <Swift/Controllers/ContactEditController.h>
 
 namespace Swift {
 
@@ -100,6 +101,7 @@ MainController::MainController(
 	chatsManager_ = NULL;
 	eventWindowController_ = NULL;
 	profileController_ = NULL;
+	contactEditController_ = NULL;
 	userSearchControllerChat_ = NULL;
 	userSearchControllerAdd_ = NULL;
 	quitRequested_ = false;
@@ -172,6 +174,8 @@ MainController::~MainController() {
 void MainController::resetClient() {
 	resetCurrentError();
 	resetPendingReconnects();
+	delete contactEditController_;
+	contactEditController_ = NULL;
 	delete profileController_;
 	profileController_ = NULL;
 	delete eventWindowController_;
@@ -233,11 +237,12 @@ void MainController::handleConnected() {
 	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));
 
+		contactEditController_ = new ContactEditController(rosterController_, uiFactory_, uiEventStream_);
+
 		chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, settings_);
 		client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1));
 		chatsManager_->setAvatarManager(client_->getAvatarManager());
@@ -268,6 +273,7 @@ void MainController::handleConnected() {
 	
 	rosterController_->setEnabled(true);
 	profileController_->setAvailable(true);
+	contactEditController_->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 */
@@ -532,6 +538,9 @@ void MainController::setManagersOffline() {
 	if (profileController_) {
 		profileController_->setAvailable(false);
 	}
+	if (contactEditController_) {
+		contactEditController_->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 d6e54ef..09f17d2 100644
--- a/Swift/Controllers/MainController.h
+++ b/Swift/Controllers/MainController.h
@@ -43,6 +43,7 @@ namespace Swift {
 	class MUCController;
 	class Notifier;
 	class ProfileController;
+	class ContactEditController;
 	class TogglableNotifier;
 	class PresenceNotifier;
 	class EventNotifier;
@@ -132,6 +133,7 @@ namespace Swift {
 			XMLConsoleController* xmlConsoleController_;
 			ChatsManager* chatsManager_;
 			ProfileController* profileController_;
+			ContactEditController* contactEditController_;
 			JID jid_;
 			JID boundJID_;
 			SystemTrayController* systemTrayController_;
diff --git a/Swift/Controllers/RosterController.cpp b/Swift/Controllers/RosterController.cpp
index 0144c97..be735cf 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"
@@ -27,10 +28,11 @@
 #include "Swiften/Roster/SetName.h"
 #include "Swiften/Roster/OfflineRosterFilter.h"
 #include "Swiften/Roster/XMPPRoster.h"
+#include "Swiften/Roster/XMPPRosterItem.h"
 #include "Swift/Controllers/UIEvents/AddContactUIEvent.h"
 #include "Swift/Controllers/UIEvents/RemoveRosterItemUIEvent.h"
 #include "Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h"
-#include "Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h"
+#include "Swift/Controllers/UIEvents/RenameGroupUIEvent.h"
 #include "Swift/Controllers/UIEvents/ToggleShowOfflineUIEvent.h"
 #include <Swiften/Client/NickManager.h>
 
@@ -165,12 +167,10 @@ void RosterController::handleOnJIDUpdated(const JID& jid, const String& oldName,
 }
 
 void RosterController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
-	boost::shared_ptr<ToggleShowOfflineUIEvent> showOfflineEvent = boost::dynamic_pointer_cast<ToggleShowOfflineUIEvent>(event);
-	if (showOfflineEvent) {
+	if (boost::shared_ptr<ToggleShowOfflineUIEvent> showOfflineEvent = boost::dynamic_pointer_cast<ToggleShowOfflineUIEvent>(event)) {
 		handleShowOfflineToggled(showOfflineEvent->getShow());
 	}
-	boost::shared_ptr<AddContactUIEvent> addContactEvent = boost::dynamic_pointer_cast<AddContactUIEvent>(event);
-	if (addContactEvent) {
+	else if (boost::shared_ptr<AddContactUIEvent> addContactEvent = boost::dynamic_pointer_cast<AddContactUIEvent>(event)) {
 		RosterItemPayload item;
 		item.setName(addContactEvent->getName());
 		item.setJID(addContactEvent->getJID());
@@ -180,10 +180,8 @@ void RosterController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
 		request->onResponse.connect(boost::bind(&RosterController::handleRosterSetError, this, _1, roster));
 		request->send();
 		subscriptionManager_->requestSubscription(addContactEvent->getJID());
-		return;
 	}
-	boost::shared_ptr<RemoveRosterItemUIEvent> removeEvent = boost::dynamic_pointer_cast<RemoveRosterItemUIEvent>(event);
-	if (removeEvent) {
+	else if (boost::shared_ptr<RemoveRosterItemUIEvent> removeEvent = boost::dynamic_pointer_cast<RemoveRosterItemUIEvent>(event)) {
 		RosterItemPayload item(removeEvent->getJID(), "", RosterItemPayload::Remove);
 		boost::shared_ptr<RosterPayload> roster(new RosterPayload());
 		roster->addItem(item);
@@ -191,10 +189,8 @@ void RosterController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
 		request->onResponse.connect(boost::bind(&RosterController::handleRosterSetError, this, _1, roster));
 		request->send();
 
-		return;
 	}
-	boost::shared_ptr<RenameRosterItemUIEvent> renameEvent = boost::dynamic_pointer_cast<RenameRosterItemUIEvent>(event);
-	if (renameEvent) {
+	else if (boost::shared_ptr<RenameRosterItemUIEvent> renameEvent = boost::dynamic_pointer_cast<RenameRosterItemUIEvent>(event)) {
 		JID contact(renameEvent->getJID());
 		RosterItemPayload item(contact, renameEvent->getNewName(), xmppRoster_->getSubscriptionStateForJID(contact));
 		item.setGroups(xmppRoster_->getGroupsForJID(contact));
@@ -203,34 +199,44 @@ void RosterController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
 		SetRosterRequest::ref request = SetRosterRequest::create(roster, iqRouter_);
 		request->onResponse.connect(boost::bind(&RosterController::handleRosterSetError, this, _1, roster));
 		request->send();
-		return;
 	}
-	boost::shared_ptr<RegroupRosterItemUIEvent> regroupEvent = boost::dynamic_pointer_cast<RegroupRosterItemUIEvent>(event);
-	if (regroupEvent) {
-		JID contact(regroupEvent->getJID());
-		RosterItemPayload item(contact, xmppRoster_->getNameForJID(contact), xmppRoster_->getSubscriptionStateForJID(contact));
-		std::vector<String> newGroups;
-		const std::vector<String> addedGroups = regroupEvent->getAddedGroups();
-		const std::vector<String> removedGroups = regroupEvent->getRemovedGroups();
-		foreach (const String& oldGroup, xmppRoster_->getGroupsForJID(contact)) {
-			if (std::find(removedGroups.begin(), removedGroups.end(), oldGroup) == removedGroups.end()
-					&& std::find(addedGroups.begin(), addedGroups.end(), oldGroup) == addedGroups.end()) {
-					newGroups.push_back(oldGroup);
-			}
+	else if (boost::shared_ptr<RenameGroupUIEvent> renameGroupEvent = boost::dynamic_pointer_cast<RenameGroupUIEvent>(event)) {
+		std::vector<XMPPRosterItem> items = xmppRoster_->getItems();
+		String group = renameGroupEvent->getGroup();
+		// FIXME: We should handle contacts groups specially to avoid clashes
+		if (group == "Contacts") {
+			group = "";
 		}
-		foreach (const String& newGroup, regroupEvent->getAddedGroups()) {
-			newGroups.push_back(newGroup);
+		foreach(XMPPRosterItem& item, items) {
+			std::vector<String> groups = item.getGroups();
+			if ( (group.isEmpty() && groups.empty()) || std::find(groups.begin(), groups.end(), group) != groups.end()) {
+				groups.erase(std::remove(groups.begin(), groups.end(), group), groups.end());
+				if (std::find(groups.begin(), groups.end(), renameGroupEvent->getNewName()) == groups.end()) {
+					groups.push_back(renameGroupEvent->getNewName());
+				}
+				item.setGroups(groups);
+				updateItem(item);
+			}
 		}
-		item.setGroups(newGroups);
-		boost::shared_ptr<RosterPayload> roster(new RosterPayload());
-		roster->addItem(item);
-		SetRosterRequest::ref request = SetRosterRequest::create(roster, iqRouter_);
-		request->onResponse.connect(boost::bind(&RosterController::handleRosterSetError, this, _1, roster));
-		request->send();
-		return;
 	}
 }
 
+void RosterController::setContactGroups(const JID& jid, const std::vector<String>& groups) {
+	updateItem(XMPPRosterItem(jid, xmppRoster_->getNameForJID(jid), groups, xmppRoster_->getSubscriptionStateForJID(jid)));
+}
+
+void RosterController::updateItem(const XMPPRosterItem& item) {
+	RosterItemPayload itemPayload(item.getJID(), item.getName(), item.getSubscription());
+	itemPayload.setGroups(item.getGroups());
+
+	RosterPayload::ref roster = boost::make_shared<RosterPayload>();
+	roster->addItem(itemPayload);
+
+	SetRosterRequest::ref request = SetRosterRequest::create(roster, iqRouter_);
+	request->onResponse.connect(boost::bind(&RosterController::handleRosterSetError, this, _1, roster));
+	request->send();
+}
+
 void RosterController::handleRosterSetError(ErrorPayload::ref error, boost::shared_ptr<RosterPayload> rosterPayload) {
 	if (!error) {
 		return;
@@ -281,4 +287,12 @@ void RosterController::handleAvatarChanged(const JID& jid) {
 	}
 }
 
+boost::optional<XMPPRosterItem> RosterController::getItem(const JID& jid) const {
+	return xmppRoster_->getItem(jid);
+}
+
+std::set<String> RosterController::getGroups() const {
+	return xmppRoster_->getGroups();
+}
+
 }
diff --git a/Swift/Controllers/RosterController.h b/Swift/Controllers/RosterController.h
index 9e22686..d8c2487 100644
--- a/Swift/Controllers/RosterController.h
+++ b/Swift/Controllers/RosterController.h
@@ -22,6 +22,7 @@ namespace Swift {
 	class IQRouter;
 	class Roster;
 	class XMPPRoster;
+	class XMPPRosterItem;
 	class MainWindow;
 	class MainWindowFactory;
 	class OfflineRosterFilter;
@@ -45,6 +46,13 @@ namespace Swift {
 			boost::signal<void ()> onSignOutRequest;
 			void handleAvatarChanged(const JID& jid);
 			void setEnabled(bool enabled);
+
+			boost::optional<XMPPRosterItem> getItem(const JID&) const;
+			std::set<String> getGroups() const;
+
+			void setContactGroups(const JID& jid, const std::vector<String>& groups);
+			void updateItem(const XMPPRosterItem&);
+
 		private:
 			void handleOnJIDAdded(const JID &jid);
 			void handleRosterCleared();
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index b149cd2..c8314de 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -30,6 +30,7 @@ if env["SCONS_STAGE"] == "build" :
 			"DiscoServiceWalker.cpp",
 			"MainController.cpp",
 			"ProfileController.cpp",
+			"ContactEditController.cpp",
 			"RosterController.cpp",
 			"RosterGroupExpandinessPersister.cpp",
 			"EventWindowController.cpp",
diff --git a/Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h b/Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h
deleted file mode 100644
index b8552b3..0000000
--- a/Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#pragma once
-
-#include <boost/shared_ptr.hpp>
-#include <vector>
-
-#include "Swift/Controllers/UIEvents/UIEvent.h"
-#include "Swiften/MUC/MUCBookmark.h"
-
-namespace Swift {
-	/**
-	 * An event for regrouping a roster item.
-	 * This doesn't need to cover all groups, so it's valid to have the
-	 * contact in several groups that are neither removedGroups or addedGroups.
-	 */
-	class RegroupRosterItemUIEvent : public UIEvent {
-		public:
-			RegroupRosterItemUIEvent(const JID& jid, const std::vector<String>& addedGroups, const std::vector<String>& removedGroups) : jid_(jid), addedGroups_(addedGroups), removedGroups_(removedGroups) {}
-
-			const JID& getJID() const {return jid_;}
-			const std::vector<String>& getAddedGroups() const {return addedGroups_;}
-			const std::vector<String>& getRemovedGroups() const {return removedGroups_;}
-
-		private:
-			JID jid_;
-			std::vector<String> addedGroups_;
-			std::vector<String> removedGroups_;
-	};
-}
diff --git a/Swift/Controllers/UIEvents/RenameGroupUIEvent.h b/Swift/Controllers/UIEvents/RenameGroupUIEvent.h
new file mode 100644
index 0000000..1825d77
--- /dev/null
+++ b/Swift/Controllers/UIEvents/RenameGroupUIEvent.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 <Swift/Controllers/UIEvents/UIEvent.h>
+#include <Swiften/Base/String.h>
+
+namespace Swift {
+	class RenameGroupUIEvent : public UIEvent {
+		public:
+			RenameGroupUIEvent(const String& group, const String& newName) : group(group), newName(newName) {
+			}
+
+			const String& getGroup() const {
+				return group;
+			}
+
+			const String& getNewName() const {
+				return newName;
+			}
+
+		private:
+			String group;
+			String newName;
+	};
+}
diff --git a/Swift/Controllers/UIEvents/RequestContactEditorUIEvent.h b/Swift/Controllers/UIEvents/RequestContactEditorUIEvent.h
new file mode 100644
index 0000000..8d04525
--- /dev/null
+++ b/Swift/Controllers/UIEvents/RequestContactEditorUIEvent.h
@@ -0,0 +1,27 @@
+/*
+ * 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/JID/JID.h>
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+
+namespace Swift {
+	class RequestContactEditorUIEvent : public UIEvent {
+		public:
+			typedef boost::shared_ptr<RequestContactEditorUIEvent> ref;
+
+			RequestContactEditorUIEvent(const JID& jid) : jid(jid) {
+			}
+
+			const JID& getJID() const {
+				return jid;
+			}
+
+		private:
+			JID jid;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/ContactEditWindow.h b/Swift/Controllers/UIInterfaces/ContactEditWindow.h
new file mode 100644
index 0000000..3feb718
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/ContactEditWindow.h
@@ -0,0 +1,33 @@
+/*
+ * 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 <set>
+#include <vector>
+
+#include <Swiften/Base/String.h>
+
+namespace Swift {
+	class JID;
+
+	class ContactEditWindow {
+		public:
+			virtual ~ContactEditWindow() {};
+
+			virtual void setEnabled(bool b) = 0;
+
+			virtual void setContact(const JID& jid, const String& name, const std::vector<String>& groups, const std::set<String>& allGroups) = 0;
+
+			virtual void show() = 0;
+			virtual void hide() = 0;
+
+			boost::signal<void ()> onRemoveContactRequest;
+			boost::signal<void (const String& /* name */, const std::vector<String>& /* groups */)> onChangeContactRequest;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h b/Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h
new file mode 100644
index 0000000..8ad56c0
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/ContactEditWindowFactory.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/ContactEditWindow.h>
+
+namespace Swift {
+	class ContactEditWindowFactory {
+		public:
+			virtual ~ContactEditWindowFactory() {};
+
+			virtual ContactEditWindow* createContactEditWindow() = 0;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h
index 11623d7..9b36ac5 100644
--- a/Swift/Controllers/UIInterfaces/UIFactory.h
+++ b/Swift/Controllers/UIInterfaces/UIFactory.h
@@ -16,6 +16,7 @@
 #include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/XMLConsoleWidgetFactory.h>
 #include <Swift/Controllers/UIInterfaces/ProfileWindowFactory.h>
+#include <Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h>
 
 namespace Swift {
 	class UIFactory : 
@@ -28,7 +29,8 @@ namespace Swift {
 			public XMLConsoleWidgetFactory, 
 			public UserSearchWindowFactory, 
 			public JoinMUCWindowFactory,
-			public ProfileWindowFactory {
+			public ProfileWindowFactory,
+			public ContactEditWindowFactory {
 		public:
 			virtual ~UIFactory() {}
 	};
diff --git a/Swift/QtUI/ChatList/QtChatListWindow.h b/Swift/QtUI/ChatList/QtChatListWindow.h
index b389474..3a3e95f 100644
--- a/Swift/QtUI/ChatList/QtChatListWindow.h
+++ b/Swift/QtUI/ChatList/QtChatListWindow.h
@@ -12,7 +12,6 @@
 #include "Swift/Controllers/UIEvents/UIEventStream.h"
 #include "Swift/QtUI/ChatList/ChatListModel.h"
 #include "Swift/QtUI/ChatList/ChatListDelegate.h"
-#include "Swift/QtUI/ContextMenus/QtContextMenu.h"
 
 namespace Swift {
 
@@ -34,13 +33,13 @@ namespace Swift {
 
 		protected:
 			void contextMenuEvent(QContextMenuEvent* event);
+
 		private:
 			void setupContextMenus();
 			bool bookmarksEnabled_;
 			UIEventStream* eventStream_;
 			ChatListModel* model_;
 			ChatListDelegate* delegate_;
-			QtContextMenu* contextMenu_;
 			QMenu* mucMenu_;
 			QMenu* emptyMenu_;
 			ChatListItem* contextMenuItem_;
diff --git a/Swift/QtUI/ContextMenus/QtContextMenu.cpp b/Swift/QtUI/ContextMenus/QtContextMenu.cpp
deleted file mode 100644
index c74fb31..0000000
--- a/Swift/QtUI/ContextMenus/QtContextMenu.cpp
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#include "ContextMenus/QtContextMenu.h"
-
-namespace Swift {
-
-QtContextMenu::~QtContextMenu() {
-}
-
-}
diff --git a/Swift/QtUI/ContextMenus/QtContextMenu.h b/Swift/QtUI/ContextMenus/QtContextMenu.h
deleted file mode 100644
index 9e73ef9..0000000
--- a/Swift/QtUI/ContextMenus/QtContextMenu.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#pragma once
-
-namespace Swift {
-	class RosterItem;
-	class QtContextMenu {
-		public:
-			virtual ~QtContextMenu();
-
-			virtual void show(RosterItem* item) = 0;
-	};
-}
diff --git a/Swift/QtUI/ContextMenus/QtRosterContextMenu.cpp b/Swift/QtUI/ContextMenus/QtRosterContextMenu.cpp
deleted file mode 100644
index 9bbbbca..0000000
--- a/Swift/QtUI/ContextMenus/QtRosterContextMenu.cpp
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#include "Swift/QtUI/ContextMenus/QtRosterContextMenu.h"
-
-#include <QInputDialog>
-#include <QLineEdit>
-#include <QMenu>
-#include <QDebug>
-#include <QDialog>
-#include <QMessageBox>
-
-#include <boost/shared_ptr.hpp>
-
-#include "Swiften/Roster/ContactRosterItem.h"
-#include "Swiften/Roster/GroupRosterItem.h"
-#include "Swiften/Base/String.h"
-#include "Swiften/Roster/Roster.h"
-#include "Swift/Controllers/UIEvents/UIEvent.h"
-#include "Swift/Controllers/UIEvents/RemoveRosterItemUIEvent.h"
-#include "Swift/Controllers/UIEvents/RenameRosterItemUIEvent.h"
-#include "Swift/QtUI/QtSwiftUtil.h"
-#include "Swift/QtUI/QtSetGroupsDialog.h"
-
-
-namespace Swift {
-
-QtRosterContextMenu::QtRosterContextMenu(UIEventStream* eventStream, QtTreeWidget* treeWidget) : eventStream_(eventStream), treeWidget_(treeWidget), item_(NULL) {
-}
-
-void QtRosterContextMenu::show(RosterItem* item) {
-	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item);
-	item_ = item;
-	QMenu contextMenu;
-	if (contact) {
-		contextMenu.addAction("Rename", this, SLOT(handleRenameContact()));
-		contextMenu.addAction("Groups", this, SLOT(handleRegroupContact()));
-		contextMenu.addSeparator();
-		contextMenu.addAction("Remove", this, SLOT(handleRemoveContact()));
-	}
-	GroupRosterItem* group = dynamic_cast<GroupRosterItem*>(item);
-	if (group) {
-		contextMenu.addAction("Rename", this, SLOT(handleRenameGroup()));
-	}
-	contextMenu.exec(QCursor::pos());
-}
-
-void QtRosterContextMenu::handleRegroupContact() {
-	QList<QString> allGroups;
-	foreach (RosterItem* item, treeWidget_->getRoster()->getRoot()->getChildren()) {
-		GroupRosterItem* group = dynamic_cast<GroupRosterItem*>(item);
-		if (group) {
-			allGroups.push_back(P2QSTRING(group->getDisplayName()));
-		}
-	}
-	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item_);
-	assert(contact);
-	QtSetGroupsDialog groupDialog(contact, allGroups);
-
-	if (groupDialog.exec() == QDialog::Accepted) {
-		eventStream_->send(groupDialog.getRegroupEvent());
-	}
-}
-
-void QtRosterContextMenu::handleRemoveContact() {
-	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item_);
-	assert(contact);
-	QMessageBox msgBox;
-	msgBox.setWindowTitle("Confirm contact deletion");
-	msgBox.setText("Are you sure you want to delete this contact?");
-	msgBox.setInformativeText(QString("This will remove the contact '%1' from all groups they may be in.").arg(P2QSTRING(contact->getJID().toString())));
-	msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
-	msgBox.setDefaultButton(QMessageBox::Yes);
-	int ret = msgBox.exec();
-	if (ret == QMessageBox::Yes) {
-		eventStream_->send(boost::shared_ptr<UIEvent>(new RemoveRosterItemUIEvent(contact->getJID())));
-	}
-}
-
-void QtRosterContextMenu::handleRenameContact() {
-	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item_);
-	assert(contact);
-	bool ok;
-	QString newName = QInputDialog::getText(NULL, "Rename contact", "New name for " + P2QSTRING(item_->getDisplayName()), QLineEdit::Normal, P2QSTRING(item_->getDisplayName()), &ok);
-	if (ok) {
-		eventStream_->send(boost::shared_ptr<UIEvent>(new RenameRosterItemUIEvent(contact->getJID(), Q2PSTRING(newName))));
-	}
-}
-
-void QtRosterContextMenu::handleRenameGroup() {
-	GroupRosterItem* group = dynamic_cast<GroupRosterItem*>(item_);
-	assert(group);
-	bool ok;
-	QString newName = QInputDialog::getText(NULL, "Rename group", "New name for " + P2QSTRING(item_->getDisplayName()), QLineEdit::Normal, P2QSTRING(item_->getDisplayName()), &ok);
-	if (ok) {
-		std::vector<String> addedGroups;
-		std::vector<String> removedGroups;
-		addedGroups.push_back(Q2PSTRING(newName));
-		removedGroups.push_back(group->getDisplayName());
-		foreach (RosterItem* child, group->getChildren()) {
-			ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(child);
-			assert(contact);
-			boost::shared_ptr<RegroupRosterItemUIEvent> regroupItem(new RegroupRosterItemUIEvent(contact->getJID(), addedGroups, removedGroups));
-			eventStream_->send(regroupItem);
-		}
-	}
-}
-
-}
diff --git a/Swift/QtUI/ContextMenus/QtRosterContextMenu.h b/Swift/QtUI/ContextMenus/QtRosterContextMenu.h
deleted file mode 100644
index 0a88a8e..0000000
--- a/Swift/QtUI/ContextMenus/QtRosterContextMenu.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#pragma once
-
-#include <QObject>
-
-#include "Swift/QtUI/ContextMenus/QtContextMenu.h"
-#include "Swift/QtUI/Roster/QtTreeWidget.h"
-#include "Swift/Controllers/UIEvents/UIEventStream.h"
-
-namespace Swift {
-	class RosterItem;
-	class QtRosterContextMenu : public QObject, public QtContextMenu {
-		Q_OBJECT
-		public:
-			QtRosterContextMenu(UIEventStream* eventStream, QtTreeWidget* treeWidget);
-			void show(RosterItem* item);
-
-		private slots:
-			void handleRemoveContact();
-			void handleRenameContact();
-			void handleRenameGroup();
-			void handleRegroupContact();
-
-		private:
-			UIEventStream* eventStream_;
-			QtTreeWidget* treeWidget_;
-			RosterItem* item_;
-	};
-}
diff --git a/Swift/QtUI/QtContactEditWindow.cpp b/Swift/QtUI/QtContactEditWindow.cpp
new file mode 100644
index 0000000..6eb4316
--- /dev/null
+++ b/Swift/QtUI/QtContactEditWindow.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "QtContactEditWindow.h"
+
+#include <algorithm>
+
+#include <QScrollArea>
+#include <QBoxLayout>
+#include <QLabel>
+#include <QCheckBox>
+#include <QLineEdit>
+#include <QDialogButtonBox>
+#include <QMessageBox>
+#include <QPushButton>
+
+#include "Swift/QtUI/QtSwiftUtil.h"
+
+namespace Swift {
+
+QtContactEditWindow::QtContactEditWindow() : groups_(NULL) {
+	resize(300,300);
+	setWindowTitle("Edit contact");
+
+	QBoxLayout* layout = new QVBoxLayout(this);
+	setContentsMargins(0,0,0,0);
+
+	jidLabel_ = new QLabel(this);
+	jidLabel_->setAlignment(Qt::AlignHCenter);
+	layout->addWidget(jidLabel_);
+
+	QHBoxLayout* nameLayout = new QHBoxLayout();
+	
+	QLabel* label = new QLabel("Name:", this);
+	nameLayout->addWidget(label);
+	name_ = new QLineEdit(this);
+	nameLayout->addWidget(name_);
+	layout->addLayout(nameLayout);
+
+	layout->addWidget(new QLabel("Groups:", this));
+
+	groupsArea_ = new QScrollArea(this);
+	layout->addWidget(groupsArea_);
+	groupsArea_->setWidgetResizable(true);
+	groupsArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+	groupsArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+
+	QHBoxLayout* buttonLayout = new QHBoxLayout();
+	layout->addLayout(buttonLayout);
+	QPushButton* removeButton = new QPushButton("Remove contact", this);
+	connect(removeButton, SIGNAL(clicked()), this, SLOT(handleRemoveContact()));
+	buttonLayout->addWidget(removeButton);
+	QPushButton* okButton = new QPushButton("Ok", this);
+	connect(okButton, SIGNAL(clicked()), this, SLOT(handleUpdateContact()));
+	buttonLayout->addStretch();
+	buttonLayout->addWidget(okButton);
+}
+
+void QtContactEditWindow::setContact(const JID& jid, const String& name, const std::vector<String>& groups, const std::set<String>& allGroups) {
+	jid_ = jid;
+	
+	jidLabel_->setText("<b>" + P2QSTRING(jid.toString()) + "</b>");
+	name_->setText(P2QSTRING(name));
+
+	delete groups_;
+	checkBoxes_.clear();
+	groups_ = new QWidget(groupsArea_);
+	groupsArea_->setWidget(groups_);
+	QVBoxLayout* scrollLayout = new QVBoxLayout(groups_);
+
+	foreach (String group, allGroups) {
+		QCheckBox* check = new QCheckBox(groups_);
+		check->setText(P2QSTRING(group));
+		check->setCheckState(Qt::Unchecked);
+		checkBoxes_[group] = check;
+		scrollLayout->addWidget(check);
+	}
+	foreach (String group, groups) {
+		checkBoxes_[group]->setCheckState(Qt::Checked);
+	}
+
+	QHBoxLayout* newGroupLayout = new QHBoxLayout();
+	newGroup_ = new QCheckBox(groups_);
+	newGroup_->setText("New Group:");
+	newGroup_->setCheckState(Qt::Unchecked);
+	newGroupLayout->addWidget(newGroup_);
+	newGroupName_ = new QLineEdit(groups_);
+	newGroupLayout->addWidget(newGroupName_);
+	scrollLayout->addLayout(newGroupLayout);
+
+	scrollLayout->addItem(new QSpacerItem(20, 73, QSizePolicy::Minimum, QSizePolicy::Expanding));
+}
+
+void QtContactEditWindow::setEnabled(bool b) {
+	QWidget::setEnabled(b);
+}
+
+void QtContactEditWindow::show() {
+	QWidget::show();
+	QWidget::activateWindow();
+}
+
+void QtContactEditWindow::hide() {
+	QWidget::hide();
+}
+
+void QtContactEditWindow::handleRemoveContact() {
+	QMessageBox msgBox;
+	msgBox.setWindowTitle("Confirm contact deletion");
+	msgBox.setText("Are you sure you want to delete this contact?");
+	msgBox.setInformativeText(QString("This will remove the contact '%1' from all groups they may be in.").arg(P2QSTRING(jid_.toString())));
+	msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+	msgBox.setDefaultButton(QMessageBox::Yes);
+	int ret = msgBox.exec();
+	if (ret == QMessageBox::Yes) {
+		onRemoveContactRequest();
+	}
+}
+
+void QtContactEditWindow::handleUpdateContact() {
+	std::vector<String> groups;
+	foreach(const CheckBoxMap::value_type& group, checkBoxes_) {
+		if (group.second->checkState() == Qt::Checked) {
+			groups.push_back(group.first);
+		}
+	}
+	if (newGroup_->checkState() == Qt::Checked && !newGroupName_->text().isEmpty()) {
+		groups.push_back(Q2PSTRING(newGroupName_->text()));
+	}
+	onChangeContactRequest(Q2PSTRING(name_->text()), groups);
+}
+
+}
diff --git a/Swift/QtUI/QtContactEditWindow.h b/Swift/QtUI/QtContactEditWindow.h
new file mode 100644
index 0000000..a731480
--- /dev/null
+++ b/Swift/QtUI/QtContactEditWindow.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <map>
+#include <boost/shared_ptr.hpp>
+
+#include <QWidget>
+
+#include <Swift/Controllers/UIInterfaces/ContactEditWindow.h>
+#include <Swiften/Base/String.h>
+#include <Swiften/JID/JID.h>
+
+class QScrollArea;
+class QLabel;
+class QLineEdit;
+class QCheckBox;
+
+namespace Swift {
+	class QtContactEditWindow : public QWidget, public ContactEditWindow {
+			Q_OBJECT
+
+		public:
+			QtContactEditWindow();
+
+			virtual void setContact(const JID& jid, const String& name, const std::vector<String>& groups, const std::set<String>& allGroups);
+
+			void setEnabled(bool);
+			void show();
+			void hide();
+
+		private slots:
+			void handleRemoveContact();
+			void handleUpdateContact();
+
+		private:
+			typedef std::map<String, QCheckBox*> CheckBoxMap;
+			JID jid_;
+			QLabel* jidLabel_;
+			CheckBoxMap checkBoxes_;
+			QLineEdit* name_;
+			QScrollArea* groupsArea_;
+			QWidget* groups_;
+			QCheckBox* newGroup_;
+			QLineEdit* newGroupName_;
+	};
+}
+
diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp
index ccf5a59..63ce32b 100644
--- a/Swift/QtUI/QtMainWindow.cpp
+++ b/Swift/QtUI/QtMainWindow.cpp
@@ -59,8 +59,6 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS
 	contactTabLayout->setContentsMargins(0, 0, 0, 0);
 
 	treeWidget_ = new QtTreeWidget(uiEventStream_);
-	contextMenu_ = new QtRosterContextMenu(uiEventStream_, treeWidget_);
-	treeWidget_->setContextMenu(contextMenu_);
 	contactTabLayout->addWidget(treeWidget_);
 
 	tabs_->addTab(contactsTabWidget_, "&Contacts");
@@ -108,7 +106,6 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS
 
 QtMainWindow::~QtMainWindow() {
 	uiEventStream_->onUIEvent.disconnect(boost::bind(&QtMainWindow::handleUIEvent, this, _1));
-	delete contextMenu_;
 }
 
 QtEventWindow* QtMainWindow::getEventWindow() {
diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h
index 67b89d8..66b0caf 100644
--- a/Swift/QtUI/QtMainWindow.h
+++ b/Swift/QtUI/QtMainWindow.h
@@ -4,8 +4,7 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
-#ifndef SWIFT_QtMainWindow_H
-#define SWIFT_QtMainWindow_H
+#pragma once
 
 #include <QWidget>
 #include <QMenu>
@@ -13,7 +12,6 @@
 #include "Swift/QtUI/QtRosterHeader.h"
 #include "Swift/QtUI/EventViewer/QtEventWindow.h"
 #include "Swift/QtUI/ChatList/QtChatListWindow.h"
-#include "Swift/QtUI/ContextMenus/QtRosterContextMenu.h"
 
 #include <vector>
 
@@ -73,10 +71,6 @@ namespace Swift {
 			QtEventWindow* eventWindow_;
 			QtChatListWindow* chatListWindow_;
 			UIEventStream* uiEventStream_;
-			QtRosterContextMenu* contextMenu_;
 			bool lastOfflineState_;
 	};
 }
-
-#endif
-
diff --git a/Swift/QtUI/QtSetGroupsDialog.cpp b/Swift/QtUI/QtSetGroupsDialog.cpp
deleted file mode 100644
index d19a55d..0000000
--- a/Swift/QtUI/QtSetGroupsDialog.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#include "QtSetGroupsDialog.h"
-
-#include <algorithm>
-
-#include <QScrollArea>
-#include <QBoxLayout>
-#include <QLabel>
-#include <QDialogButtonBox>
-
-#include "Swift/QtUI/QtSwiftUtil.h"
-
-namespace Swift {
-
-QtSetGroupsDialog::QtSetGroupsDialog(ContactRosterItem* contact, const QList<QString>& allGroups) : contact_(contact) {
-	//resize(300,300);
-	setWindowTitle("Edit contact");
-	QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, this);
-	setContentsMargins(0,0,0,0);
-	QScrollArea* scrollArea = new QScrollArea(this);
-	layout->addWidget(scrollArea);
-	scrollArea->setWidgetResizable(true);
-	scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
-	scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
-	QWidget* scroll = new QWidget(scrollArea);
-	scrollArea->setWidget(scroll);
-	QVBoxLayout* scrollLayout = new QVBoxLayout(scroll);
-	QLabel* label = new QLabel(scroll);
-	label->setText("Choose groups for " + P2QSTRING(contact->getDisplayName()) + " (" + P2QSTRING(contact->getJID().toString()) + ")");
-	scrollLayout->addWidget(label);
-	foreach (QString group, allGroups) {
-		QCheckBox* check = new QCheckBox(scroll);
-		check->setText(group);
-		check->setCheckState(Qt::Unchecked);
-		checkBoxes_[Q2PSTRING(group)] = check;
-		scrollLayout->addWidget(check);
-	}
-	foreach (String group, contact->getGroups()) {
-		checkBoxes_[group]->setCheckState(Qt::Checked);
-	}
-
-	QHBoxLayout* newGroupLayout = new QHBoxLayout();
-	newGroup_ = new QCheckBox(scroll);
-	newGroup_->setText("New Group:");
-	newGroup_->setCheckState(Qt::Unchecked);
-	newGroupLayout->addWidget(newGroup_);
-	newGroupName_ = new QLineEdit(scroll);
-	newGroupLayout->addWidget(newGroupName_);
-	scrollLayout->addLayout(newGroupLayout);
-
-	scrollLayout->addItem(new QSpacerItem(20, 73, QSizePolicy::Minimum, QSizePolicy::Expanding));
-
-	QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, this);
-	layout->addWidget(buttons);
-	connect(buttons, SIGNAL(accepted()), this, SLOT(accept()));
-	connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
-}
-
-QtSetGroupsDialog::~QtSetGroupsDialog() {
-	// TODO Auto-generated destructor stub
-}
-
-typedef std::pair<String, QCheckBox*> CheckStringPair;
-
-boost::shared_ptr<RegroupRosterItemUIEvent> QtSetGroupsDialog::getRegroupEvent() {
-	std::vector<String> addedGroups;
-	std::vector<String> removedGroups;
-	std::vector<String> existingGroups = contact_->getGroups();
-	int tickedCount = 0;
-	bool wantsContacts = false;
-	foreach (CheckStringPair pair, checkBoxes_) {
-		bool existing = std::find(existingGroups.begin(), existingGroups.end(), pair.first) != existingGroups.end();
-		if (pair.second->checkState() == Qt::Checked) {
-			tickedCount++;
-			if (pair.first == "Contacts") {
-				wantsContacts = true;
-			}
-			if (!existing && pair.first != "Contacts") {
-				addedGroups.push_back(pair.first);
-			}
-		} else {
-			if (existing) {
-				removedGroups.push_back(pair.first);
-			}
-		}
-	}
-	if (newGroup_->checkState() == Qt::Checked) {
-		tickedCount++;
-		String name = Q2PSTRING(newGroupName_->text());
-		if (std::find(existingGroups.begin(), existingGroups.end(), name) == existingGroups.end()) {
-			addedGroups.push_back(name);
-		}
-	}
-	if (tickedCount > 1 && wantsContacts) {
-		addedGroups.push_back("Contacts");
-	}
-	boost::shared_ptr<RegroupRosterItemUIEvent> result(new RegroupRosterItemUIEvent(contact_->getJID(), addedGroups, removedGroups));
-	return result;
-}
-
-}
diff --git a/Swift/QtUI/QtSetGroupsDialog.h b/Swift/QtUI/QtSetGroupsDialog.h
deleted file mode 100644
index e8300f5..0000000
--- a/Swift/QtUI/QtSetGroupsDialog.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2010 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-#pragma once
-
-#include <map>
-
-#include <boost/shared_ptr.hpp>
-
-#include <QDialog>
-#include <QCheckBox>
-#include <QLineEdit>
-#include <QList>
-
-#include "Swiften/Roster/ContactRosterItem.h"
-#include "Swift/Controllers/UIEvents/RegroupRosterItemUIEvent.h"
-
-namespace Swift {
-
-class QtSetGroupsDialog : public QDialog {
-	Q_OBJECT
-	public:
-		QtSetGroupsDialog(ContactRosterItem* contact, const QList<QString>& allGroups);
-		virtual ~QtSetGroupsDialog();
-		boost::shared_ptr<RegroupRosterItemUIEvent> getRegroupEvent();
-	private:
-		ContactRosterItem* contact_;
-		std::map<String, QCheckBox*> checkBoxes_;
-		QCheckBox* newGroup_;
-		QLineEdit* newGroupName_;
-};
-
-}
-
diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp
index 35dc4ea..4832205 100644
--- a/Swift/QtUI/QtUIFactory.cpp
+++ b/Swift/QtUI/QtUIFactory.cpp
@@ -22,6 +22,7 @@
 #include "MUCSearch/QtMUCSearchWindow.h"
 #include "UserSearch/QtUserSearchWindow.h"
 #include "QtProfileWindow.h"
+#include "QtContactEditWindow.h"
 
 namespace Swift {
 
@@ -94,4 +95,9 @@ ProfileWindow* QtUIFactory::createProfileWindow() {
 	return new QtProfileWindow();
 }
 
+ContactEditWindow* QtUIFactory::createContactEditWindow() {
+	return new QtContactEditWindow();
+}
+
+
 }
diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h
index 199cebf..aa88fa3 100644
--- a/Swift/QtUI/QtUIFactory.h
+++ b/Swift/QtUI/QtUIFactory.h
@@ -36,6 +36,7 @@ namespace Swift {
 			virtual UserSearchWindow* createUserSearchWindow(UserSearchWindow::Type type, UIEventStream* eventStream);
 			virtual JoinMUCWindow* createJoinMUCWindow();
 			virtual ProfileWindow* createProfileWindow();
+			virtual ContactEditWindow* createContactEditWindow();
 
 		private slots:
 			void handleLoginWindowGeometryChanged();
diff --git a/Swift/QtUI/Roster/QtTreeWidget.cpp b/Swift/QtUI/Roster/QtTreeWidget.cpp
index cecb557..3c0b303 100644
--- a/Swift/QtUI/Roster/QtTreeWidget.cpp
+++ b/Swift/QtUI/Roster/QtTreeWidget.cpp
@@ -6,15 +6,19 @@
 
 #include "Roster/QtTreeWidget.h"
 
+#include <QMenu>
+#include <QContextMenuEvent>
+#include <QInputDialog>
+#include <boost/smart_ptr/make_shared.hpp>
+
 #include "Swiften/Base/Platform.h"
 #include "Swiften/Roster/ContactRosterItem.h"
 #include "Swiften/Roster/GroupRosterItem.h"
 #include "Swift/Controllers/UIEvents/UIEventStream.h"
 #include "Swift/Controllers/UIEvents/RequestChatUIEvent.h"
-
-#include <qdebug.h>
-#include <QMenu>
-#include <QContextMenuEvent>
+#include "Swift/Controllers/UIEvents/RequestContactEditorUIEvent.h"
+#include "Swift/Controllers/UIEvents/RenameGroupUIEvent.h"
+#include "QtSwiftUtil.h"
 
 namespace Swift {
 
@@ -25,7 +29,6 @@ QtTreeWidget::QtTreeWidget(UIEventStream* eventStream, QWidget* parent) : QTreeV
 	delegate_ = new RosterDelegate(this);
 	setItemDelegate(delegate_);
 	setHeaderHidden(true);
-	contextMenu_ = NULL;
 #ifdef SWIFT_PLATFORM_MACOSX
 	setAlternatingRowColors(true);
 #endif
@@ -52,10 +55,6 @@ void QtTreeWidget::setRosterModel(Roster* roster) {
 	expandAll();
 }
 
-void QtTreeWidget::setContextMenu(QtContextMenu* contextMenu) {
-	contextMenu_ = contextMenu;
-}
-
 QtTreeWidgetItem* QtTreeWidget::getRoot() {
 	return treeRoot_;
 }
@@ -77,13 +76,29 @@ void QtTreeWidget::handleItemActivated(const QModelIndex& index) {
 }
 
 void QtTreeWidget::contextMenuEvent(QContextMenuEvent* event) {
-	if (!contextMenu_) {
+	QModelIndex index = indexAt(event->pos());
+	if (!index.isValid()) {
 		return;
 	}
-	QModelIndex index = indexAt(event->pos());
-	RosterItem* item = index.isValid() ? static_cast<RosterItem*>(index.internalPointer()) : NULL;
-	if (item) {
-		contextMenu_->show(item);
+	RosterItem* item = static_cast<RosterItem*>(index.internalPointer());
+	QMenu contextMenu;
+	if (ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item)) {
+		QAction* editContact = contextMenu.addAction("Edit");
+		QAction* result = contextMenu.exec(event->globalPos());
+		if (result == editContact) {
+			eventStream_->send(boost::make_shared<RequestContactEditorUIEvent>(contact->getJID()));
+		}
+	}
+	else if (GroupRosterItem* group = dynamic_cast<GroupRosterItem*>(item)) {
+		QAction* renameGroup = contextMenu.addAction("Rename");
+		QAction* result = contextMenu.exec(event->globalPos());
+		if (result == renameGroup) {
+			bool ok;
+			QString newName = QInputDialog::getText(NULL, "Rename group", "New name for " + P2QSTRING(group->getDisplayName()), QLineEdit::Normal, P2QSTRING(group->getDisplayName()), &ok);
+			if (ok) {
+				eventStream_->send(boost::make_shared<RenameGroupUIEvent>(group->getDisplayName(), Q2PSTRING(newName)));
+			}
+		}
 	}
 }
 
diff --git a/Swift/QtUI/Roster/QtTreeWidget.h b/Swift/QtUI/Roster/QtTreeWidget.h
index 838c453..ff11567 100644
--- a/Swift/QtUI/Roster/QtTreeWidget.h
+++ b/Swift/QtUI/Roster/QtTreeWidget.h
@@ -12,7 +12,6 @@
 #include "Swift/QtUI/Roster/QtTreeWidget.h"
 #include "Swift/QtUI/Roster/RosterModel.h"
 #include "Swift/QtUI/Roster/RosterDelegate.h"
-#include "Swift/QtUI/ContextMenus/QtContextMenu.h"
 
 namespace Swift {
 class UIEventStream;
@@ -24,7 +23,6 @@ class QtTreeWidget : public QTreeView{
 		~QtTreeWidget();
 		void show();
 		QtTreeWidgetItem* getRoot();
-		void setContextMenu(QtContextMenu* contextMenu);
 		void setRosterModel(Roster* roster);
 		Roster* getRoster() {return roster_;}
 	private slots:
@@ -42,7 +40,6 @@ class QtTreeWidget : public QTreeView{
 		Roster* roster_;
 		RosterDelegate* delegate_;
 		QtTreeWidgetItem* treeRoot_;
-		QtContextMenu* contextMenu_;
 		UIEventStream* eventStream_;
 };
 
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index b16fb7d..d27dc8c 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -90,7 +90,7 @@ sources = [
     "QtBookmarkDetailWindow.cpp",
     "QtAddBookmarkWindow.cpp",
     "QtEditBookmarkWindow.cpp",
-    "QtSetGroupsDialog.cpp",
+    "QtContactEditWindow.cpp",
     "ChatSnippet.cpp",
     "MessageSnippet.cpp",
     "SystemMessageSnippet.cpp",
@@ -120,8 +120,6 @@ sources = [
     "UserSearch/QtUserSearchWindow.cpp",
     "UserSearch/UserSearchModel.cpp",
     "UserSearch/UserSearchDelegate.cpp",
-    "ContextMenus/QtRosterContextMenu.cpp",
-    "ContextMenus/QtContextMenu.cpp",
     "QtSubscriptionRequestWindow.cpp",
     "QtRosterHeader.cpp",
     "QtWebView.cpp",
diff --git a/Swiften/Roster/XMPPRoster.h b/Swiften/Roster/XMPPRoster.h
index b88148d..676e8f9 100644
--- a/Swiften/Roster/XMPPRoster.h
+++ b/Swiften/Roster/XMPPRoster.h
@@ -6,12 +6,15 @@
 
 #pragma once
 
+#include <boost/optional.hpp>
+#include <vector>
+#include <set>
+#include "Swiften/Base/boost_bsignals.h"
+
 #include "Swiften/Base/String.h"
 #include "Swiften/JID/JID.h"
 #include "Swiften/Elements/RosterItemPayload.h"
-
-#include <vector>
-#include "Swiften/Base/boost_bsignals.h"
+#include <Swiften/Roster/XMPPRosterItem.h>
 
 namespace Swift {
 	/**
@@ -43,7 +46,22 @@ namespace Swift {
 			/**
 			 * Returns the list of groups for the given JID.
 			 */
-			virtual const std::vector<String>& getGroupsForJID(const JID& jid) = 0;
+			virtual std::vector<String> getGroupsForJID(const JID& jid) = 0;
+
+			/**
+			 * Retrieve the items in the roster.
+			 */
+			virtual std::vector<XMPPRosterItem> getItems() const = 0;
+
+			/**
+			 * Retrieve the item with the given JID.
+			 */
+			virtual boost::optional<XMPPRosterItem> getItem(const JID&) const = 0;
+
+			/**
+			 * Retrieve the list of (existing) groups.
+			 */
+			virtual std::set<String> getGroups() const = 0;
 
 		public:
 			/**
diff --git a/Swiften/Roster/XMPPRosterImpl.cpp b/Swiften/Roster/XMPPRosterImpl.cpp
index 762ae29..3e9e312 100644
--- a/Swiften/Roster/XMPPRosterImpl.cpp
+++ b/Swiften/Roster/XMPPRosterImpl.cpp
@@ -4,7 +4,8 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
-#include "Swiften/Roster/XMPPRosterImpl.h"
+#include <Swiften/Roster/XMPPRosterImpl.h>
+#include <Swiften/Base/foreach.h>
 
 namespace Swift {
 
@@ -13,21 +14,15 @@ XMPPRosterImpl::XMPPRosterImpl() {
 
 void XMPPRosterImpl::addContact(const JID& jid, const String& name, const std::vector<String>& groups, RosterItemPayload::Subscription subscription) {
 	JID bareJID(jid.toBare());
-	bool exists = containsJID(bareJID);
-	String oldName = getNameForJID(bareJID);
-	std::vector<String> oldGroups = entries_[bareJID].groups;
-	if (exists) {
-		entries_.erase(bareJID);
-	}
-	XMPPRosterItem item;
-	item.groups = groups;
-	item.name = name;
-	item.jid = jid;
-	item.subscription = subscription;
-	entries_[bareJID] = item;
-	if (exists) {
+	std::map<JID, XMPPRosterItem>::iterator i = entries_.find(bareJID);
+	if (i != entries_.end()) {
+		String oldName = i->second.getName();
+		std::vector<String> oldGroups = i->second.getGroups();
+		i->second = XMPPRosterItem(jid, name, groups, subscription);
 		onJIDUpdated(bareJID, oldName, oldGroups);
-	} else {
+	}
+	else {
+		entries_.insert(std::make_pair(bareJID, XMPPRosterItem(jid, name, groups, subscription)));
 		onJIDAdded(bareJID);
 	}
 }
@@ -49,19 +44,58 @@ bool XMPPRosterImpl::containsJID(const JID& jid) {
 String XMPPRosterImpl::getNameForJID(const JID& jid) const {
 	std::map<JID, XMPPRosterItem>::const_iterator i = entries_.find(jid.toBare());
 	if (i != entries_.end()) {
-		return i->second.name;
+		return i->second.getName();
 	}
 	else {
 		return "";
 	}
 }
 
-const std::vector<String>& XMPPRosterImpl::getGroupsForJID(const JID& jid) {
-	return entries_[JID(jid.toBare())].groups;
+std::vector<String> XMPPRosterImpl::getGroupsForJID(const JID& jid) {
+	std::map<JID, XMPPRosterItem>::iterator i = entries_.find(jid.toBare());
+	if (i != entries_.end()) {
+		return i->second.getGroups();
+	}
+	else {
+		return std::vector<String>();
+	}
 }
 
 RosterItemPayload::Subscription XMPPRosterImpl::getSubscriptionStateForJID(const JID& jid) {
-	return entries_[JID(jid.toBare())].subscription;
+	std::map<JID, XMPPRosterItem>::iterator i = entries_.find(jid.toBare());
+	if (i != entries_.end()) {
+		return i->second.getSubscription();
+	}
+	else {
+		return RosterItemPayload::None;
+	}
+}
+
+std::vector<XMPPRosterItem> XMPPRosterImpl::getItems() const {
+	std::vector<XMPPRosterItem> result;
+	foreach(const RosterMap::value_type& entry, entries_) {
+		result.push_back(entry.second);
+	}
+	return result;
+}
+
+boost::optional<XMPPRosterItem> XMPPRosterImpl::getItem(const JID& jid) const {
+	std::map<JID, XMPPRosterItem>::const_iterator i = entries_.find(jid.toBare());
+	if (i != entries_.end()) {
+		return i->second;
+	}
+	else {
+		return boost::optional<XMPPRosterItem>();
+	}
+}
+
+std::set<String> XMPPRosterImpl::getGroups() const {
+	std::set<String> result;
+	foreach(const RosterMap::value_type& entry, entries_) {
+		std::vector<String> groups = entry.second.getGroups();
+		result.insert(groups.begin(), groups.end());
+	}
+	return result;
 }
 
 }
diff --git a/Swiften/Roster/XMPPRosterImpl.h b/Swiften/Roster/XMPPRosterImpl.h
index c2d2458..f65683f 100644
--- a/Swiften/Roster/XMPPRosterImpl.h
+++ b/Swiften/Roster/XMPPRosterImpl.h
@@ -7,6 +7,7 @@
 #pragma once
 
 #include <map>
+#include <set>
 
 #include "Swiften/Roster/XMPPRoster.h"
 
@@ -22,15 +23,14 @@ namespace Swift {
 			bool containsJID(const JID& jid);
 			RosterItemPayload::Subscription getSubscriptionStateForJID(const JID& jid);
 			String getNameForJID(const JID& jid) const;
-			const std::vector<String>& getGroupsForJID(const JID& jid);
+			std::vector<String> getGroupsForJID(const JID& jid);
+
+			virtual std::vector<XMPPRosterItem> getItems() const;
+			virtual boost::optional<XMPPRosterItem> getItem(const JID&) const;
+			virtual std::set<String> getGroups() const;
 
 		private:
-			struct XMPPRosterItem {
-				JID jid;
-				String name;
-				std::vector<String> groups;
-				RosterItemPayload::Subscription subscription;
-			};
-			std::map<JID, XMPPRosterItem> entries_;
+			typedef std::map<JID, XMPPRosterItem> RosterMap;
+			RosterMap entries_;
 	};
 }
diff --git a/Swiften/Roster/XMPPRosterItem.h b/Swiften/Roster/XMPPRosterItem.h
new file mode 100644
index 0000000..ceb7763
--- /dev/null
+++ b/Swiften/Roster/XMPPRosterItem.h
@@ -0,0 +1,53 @@
+/*
+ * 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 <vector>
+
+#include <Swiften/Base/String.h>
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/RosterItemPayload.h>
+
+namespace Swift {
+	class XMPPRosterItem {
+		public:
+			XMPPRosterItem(const JID& jid, const String& name, const std::vector<String>& groups, RosterItemPayload::Subscription subscription) : jid(jid), name(name), groups(groups), subscription(subscription) {
+			}
+
+			const JID& getJID() const {
+				return jid;
+			}
+
+			const String& getName() const {
+				return name;
+			}
+
+			void setName(const String& name) {
+				this->name = name;
+			}
+
+			const std::vector<String>& getGroups() const {
+				return groups;
+			}
+
+			void setGroups(const std::vector<String>& groups) {
+				this->groups = groups;
+			}
+
+			RosterItemPayload::Subscription getSubscription() const {
+				return subscription;
+			}
+
+		private:
+			JID jid;
+			String name;
+			std::vector<String> groups;
+			RosterItemPayload::Subscription subscription;
+	};
+}
+
-- 
cgit v0.10.2-6-g49f6