From f2bcc401477dcb5ca52b5d9d5e85f4bf7bae9285 Mon Sep 17 00:00:00 2001
From: Richard Maudsley <richard.maudsley@isode.com>
Date: Mon, 13 Jan 2014 15:26:24 +0000
Subject: Reworked highlight rules dialog. Added support for highlighting
 individual words in messages.

Change-Id: I378fa69077c29008db4ef7c2265e5212924bc2ce

diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp
index 2367761..9df7708 100644
--- a/Swift/Controllers/Chat/ChatController.cpp
+++ b/Swift/Controllers/Chat/ChatController.cpp
@@ -49,7 +49,7 @@ namespace Swift {
 /**
  * The controller does not gain ownership of the stanzaChannel, nor the factory.
  */
-ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider)
+ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider)
 	: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) {
 	isInMUC_ = isInMUC;
 	lastWasPresence_ = false;
@@ -318,7 +318,7 @@ void ChatController::postSendMessage(const std::string& body, boost::shared_ptr<
 	boost::shared_ptr<Replace> replace = sentStanza->getPayload<Replace>();
 	if (replace) {
 		eraseIf(unackedStanzas_, PairSecondEquals<boost::shared_ptr<Stanza>, std::string>(myLastMessageUIID_));
-		replaceMessage(body, myLastMessageUIID_, boost::posix_time::microsec_clock::universal_time(), HighlightAction());
+		replaceMessage(body, myLastMessageUIID_, true, boost::posix_time::microsec_clock::universal_time(), HighlightAction());
 	} else {
 		myLastMessageUIID_ = addMessage(body, QT_TRANSLATE_NOOP("", "me"), true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel().getLabel() : boost::shared_ptr<SecurityLabel>(), avatarManager_->getAvatarPath(selfJID_), boost::posix_time::microsec_clock::universal_time(), HighlightAction());
 	}
diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h
index f8b6d8b..8b1bb9a 100644
--- a/Swift/Controllers/Chat/ChatController.h
+++ b/Swift/Controllers/Chat/ChatController.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
+ * Copyright (c) 2010-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -28,7 +28,7 @@ namespace Swift {
 
 	class ChatController : public ChatControllerBase {
 		public:
-			ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider);
+			ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider);
 			virtual ~ChatController();
 			virtual void setToJID(const JID& jid);
 			virtual void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info);
diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp
index 23137dc..5363e0c 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.cpp
+++ b/Swift/Controllers/Chat/ChatControllerBase.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
+ * Copyright (c) 2010-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -42,7 +42,7 @@
 
 namespace Swift {
 
-ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) {
+ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) {
 	chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream);
 	chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this));
 	chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2));
@@ -201,15 +201,15 @@ std::string ChatControllerBase::addMessage(const std::string& message, const std
 	if (boost::starts_with(message, "/me ")) {
 		return chatWindow_->addAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight);
 	} else {
-		return chatWindow_->addMessage(chatMessageParser_->parseMessageBody(message), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight);
+		return chatWindow_->addMessage(chatMessageParser_->parseMessageBody(message,senderIsSelf), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight);
 	}
 }
 
-void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
+void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, bool senderIsSelf, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
 	if (boost::starts_with(message, "/me ")) {
 		chatWindow_->replaceWithAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), id, time, highlight);
 	} else {
-		chatWindow_->replaceMessage(chatMessageParser_->parseMessageBody(message), id, time, highlight);
+		chatWindow_->replaceMessage(chatMessageParser_->parseMessageBody(message,senderIsSelf), id, time, highlight);
 	}
 }
 
@@ -280,7 +280,7 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m
 			std::map<JID, std::string>::iterator lastMessage;
 			lastMessage = lastMessagesUIID_.find(from);
 			if (lastMessage != lastMessagesUIID_.end()) {
-				replaceMessage(body, lastMessagesUIID_[from], timeStamp, highlight);
+				replaceMessage(body, lastMessagesUIID_[from], isIncomingMessageFromMe(message), timeStamp, highlight);
 			}
 		}
 		else {
diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h
index 7db94a4..cf0a4d2 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.h
+++ b/Swift/Controllers/Chat/ChatControllerBase.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
+ * Copyright (c) 2010-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -55,7 +55,7 @@ namespace Swift {
 			virtual void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info);
 			void handleIncomingMessage(boost::shared_ptr<MessageEvent> message);
 			std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight);
-			void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight);
+			void replaceMessage(const std::string& message, const std::string& id, bool senderIsSelf, const boost::posix_time::ptime& time, const HighlightAction& highlight);
 			virtual void setOnline(bool online);
 			virtual void setEnabled(bool enabled);
 			virtual void setToJID(const JID& jid) {toJID_ = jid;}
@@ -70,7 +70,7 @@ namespace Swift {
 			boost::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC;
 
 		protected:
-			ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider);
+			ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider);
 
 			/**
 			 * Pass the Message appended, and the stanza used to send it.
@@ -127,7 +127,7 @@ namespace Swift {
 			HistoryController* historyController_;
 			MUCRegistry* mucRegistry_;
 			Highlighter* highlighter_;
-			ChatMessageParser* chatMessageParser_;
+			boost::shared_ptr<ChatMessageParser> chatMessageParser_;
 			AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_;
 			UIEventStream* eventStream_;
 	};
diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp
index 698b766..09d93ac 100644
--- a/Swift/Controllers/Chat/ChatMessageParser.cpp
+++ b/Swift/Controllers/Chat/ChatMessageParser.cpp
@@ -20,13 +20,13 @@
 
 namespace Swift {
 
-	ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons) : emoticons_(emoticons) {
-
+	ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode)
+	: emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) {
 	}
 
 	typedef std::pair<std::string, std::string> StringPair;
 
-	ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body) {
+	ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, bool senderIsSelf) {
 		ChatWindow::ChatMessage parsedMessage;
 		std::string remaining = body;
 		/* Parse one, URLs */
@@ -51,8 +51,21 @@ namespace Swift {
 				}
 			}
 		}
-		
 
+		/* do emoticon substitution */
+		parsedMessage = emoticonHighlight(parsedMessage);
+
+		if (!senderIsSelf) { /* do not highlight our own messsages */
+			/* do word-based color highlighting */
+			parsedMessage = splitHighlight(parsedMessage);
+		}
+
+		return parsedMessage;
+	}
+
+	ChatWindow::ChatMessage ChatMessageParser::emoticonHighlight(const ChatWindow::ChatMessage& message)
+	{
+		ChatWindow::ChatMessage parsedMessage = message;
 
 		std::string regexString;
 		/* Parse two, emoticons */
@@ -124,4 +137,58 @@ namespace Swift {
 		}
 		return parsedMessage;
 	}
+
+	ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message)
+	{
+		ChatWindow::ChatMessage parsedMessage = message;
+
+		for (size_t i = 0; i < highlightRules_->getSize(); ++i) {
+			const HighlightRule& rule = highlightRules_->getRule(i);
+			if (rule.getMatchMUC() && !mucMode_) {
+				continue; /* this rule only applies to MUC's, and this is a CHAT */
+			} else if (rule.getMatchChat() && mucMode_) {
+				continue; /* this rule only applies to CHAT's, and this is a MUC */
+			}
+			foreach(const boost::regex &regex, rule.getKeywordRegex()) {
+				ChatWindow::ChatMessage newMessage;
+				foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, parsedMessage.getParts()) {
+					boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart;
+					if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) {
+						try {
+							boost::match_results<std::string::const_iterator> match;
+							const std::string& text = textPart->text;
+							std::string::const_iterator start = text.begin();
+							while (regex_search(start, text.end(), match, regex)) {
+								std::string::const_iterator matchStart = match[0].first;
+								std::string::const_iterator matchEnd = match[0].second;
+								if (start != matchStart) {
+									/* If we're skipping over plain text since the previous emoticon, record it as plain text */
+									newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart)));
+								}
+								boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = boost::make_shared<ChatWindow::ChatHighlightingMessagePart>();
+								highlightPart->text = match.str();
+								highlightPart->foregroundColor = rule.getAction().getTextColor();
+								highlightPart->backgroundColor = rule.getAction().getTextBackground();
+								newMessage.append(highlightPart);
+								start = matchEnd;
+							}
+							if (start != text.end()) {
+								/* If there's plain text after the last emoticon, record it */
+								newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end())));
+							}
+						}
+						catch (std::runtime_error) {
+							/* Basically too expensive to compute the regex results and it gave up, so pass through as text */
+							newMessage.append(part);
+						}
+					} else {
+						newMessage.append(part);
+					}
+				}
+				parsedMessage = newMessage;
+			}
+		}
+
+		return parsedMessage;
+	}
 }
diff --git a/Swift/Controllers/Chat/ChatMessageParser.h b/Swift/Controllers/Chat/ChatMessageParser.h
index c9b9456..cff4ffa 100644
--- a/Swift/Controllers/Chat/ChatMessageParser.h
+++ b/Swift/Controllers/Chat/ChatMessageParser.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Kevin Smith
+ * Copyright (c) 2013-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -14,10 +14,13 @@ namespace Swift {
 
 	class ChatMessageParser {
 		public:
-			ChatMessageParser(const std::map<std::string, std::string>& emoticons);
-			ChatWindow::ChatMessage parseMessageBody(const std::string& body);
+			ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode = false);
+			ChatWindow::ChatMessage parseMessageBody(const std::string& body, bool senderIsSelf = false);
 		private:
+			ChatWindow::ChatMessage emoticonHighlight(const ChatWindow::ChatMessage& parsedMessage);
+			ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage);
 			std::map<std::string, std::string> emoticons_;
-
+			HighlightRulesListPtr highlightRules_;
+			bool mucMode_;
 	};
 }
diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp
index 1698b4a..8a077d1 100644
--- a/Swift/Controllers/Chat/ChatsManager.cpp
+++ b/Swift/Controllers/Chat/ChatsManager.cpp
@@ -145,6 +145,7 @@ ChatsManager::ChatsManager(
 			historyController_(historyController),
 			whiteboardManager_(whiteboardManager),
 			highlightManager_(highlightManager),
+			emoticons_(emoticons),
 			clientBlockListManager_(clientBlockListManager),
 			inviteUserSearchController_(inviteUserSearchController),
 			vcardManager_(vcardManager) {
@@ -161,7 +162,6 @@ ChatsManager::ChatsManager(
 	uiEventStream_ = uiEventStream;
 	mucBookmarkManager_ = NULL;
 	profileSettings_ = profileSettings;
-	chatMessageParser_ = new ChatMessageParser(emoticons);
 	presenceOracle_->onPresenceChange.connect(boost::bind(&ChatsManager::handlePresenceChange, this, _1));
 	uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&ChatsManager::handleUIEvent, this, _1));
 
@@ -208,7 +208,6 @@ ChatsManager::~ChatsManager() {
 	}
 	delete mucBookmarkManager_;
 	delete mucSearchController_;
-	delete chatMessageParser_;
 	delete autoAcceptMUCInviteDecider_;
 }
 
@@ -697,7 +696,8 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact)
 
 ChatController* ChatsManager::createNewChatController(const JID& contact) {
 	assert(chatControllers_.find(contact) == chatControllers_.end());
-	ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, autoAcceptMUCInviteDecider_);
+	boost::shared_ptr<ChatMessageParser> chatMessageParser = boost::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), false); /* a message parser that knows this is a chat (not a room/MUC) */
+	ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_);
 	chatControllers_[contact] = controller;
 	controller->setAvailableServerFeatures(serverDiscoInfo_);
 	controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false));
@@ -781,7 +781,8 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti
 		if (reuseChatwindow) {
 			chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow);
 		}
-		controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, chatMessageParser_, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_);
+		boost::shared_ptr<ChatMessageParser> chatMessageParser = boost::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), true); /* a message parser that knows this is a room/MUC (not a chat) */
+		controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_);
 		if (chatWindowFactoryAdapter) {
 			/* The adapters are only passed to chat windows, which are deleted in their
 			 * controllers' dtor, which are deleted in ChatManager's dtor. The adapters
diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h
index 88a0986..41435d9 100644
--- a/Swift/Controllers/Chat/ChatsManager.h
+++ b/Swift/Controllers/Chat/ChatsManager.h
@@ -174,8 +174,8 @@ namespace Swift {
 			HistoryController* historyController_;
 			WhiteboardManager* whiteboardManager_;
 			HighlightManager* highlightManager_;
+			std::map<std::string, std::string> emoticons_;
 			ClientBlockListManager* clientBlockListManager_;
-			ChatMessageParser* chatMessageParser_;
 			JID localMUCServiceJID_;
 			boost::shared_ptr<DiscoServiceWalker> localMUCServiceFinderWalker_;
 			AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_;
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index d09bc3d..b467227 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -71,7 +71,7 @@ MUCController::MUCController (
 		HistoryController* historyController,
 		MUCRegistry* mucRegistry,
 		HighlightManager* highlightManager,
-		ChatMessageParser* chatMessageParser,
+		boost::shared_ptr<ChatMessageParser> chatMessageParser,
 		bool isImpromptu,
 		AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider,
 		VCardManager* vcardManager) :
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index feffaba..b5b5837 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -50,7 +50,7 @@ namespace Swift {
 
 	class MUCController : public ChatControllerBase {
 		public:
-			MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* roster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ChatMessageParser* chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager);
+			MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* roster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager);
 			virtual ~MUCController();
 			boost::signal<void ()> onUserLeft;
 			boost::signal<void ()> onUserJoined;
diff --git a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp
index 0a14303..5dca63a 100644
--- a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp
+++ b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp
@@ -48,28 +48,136 @@ public:
 		CPPUNIT_ASSERT_EQUAL(path, part->imagePath);
 	}
 
+	void assertHighlight(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) {
+		boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(result.getParts()[index]);
+		CPPUNIT_ASSERT_EQUAL(text, part->text);
+	}
+
 	void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) {
 		boost::shared_ptr<ChatWindow::ChatURIMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(result.getParts()[index]);
 		CPPUNIT_ASSERT_EQUAL(text, part->target);
 	}
 
+	static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord)
+	{
+		HighlightRule rule;
+		std::vector<std::string> keywords;
+		keywords.push_back(keyword);
+		rule.setKeywords(keywords);
+		rule.setMatchCase(matchCase);
+		rule.setMatchWholeWords(matchWholeWord);
+		rule.setMatchChat(true);
+		return rule;
+	}
+
+	static const HighlightRulesListPtr ruleListFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord)
+	{
+		boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>();
+		list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord));
+		return list;
+	}
+
+	static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2)
+	{
+		boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>();
+		list->addRule(rule1);
+		list->addRule(rule2);
+		return list;
+	}
+
 	void testFullBody() {
-		ChatMessageParser testling(emoticons_);
-		ChatWindow::ChatMessage result = testling.parseMessageBody(":) shiny :( :) http://wonderland.lit/blah http://denmark.lit boom boom");
+		const std::string no_special_message = "a message with no special content";
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
+		ChatWindow::ChatMessage result = testling.parseMessageBody(no_special_message);
+		assertText(result, 0, no_special_message);
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false));
+		result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom");
 		assertEmoticon(result, 0, smile1_, smile1Path_);
 		assertText(result, 1, " shiny ");
 		assertEmoticon(result, 2, smile2_, smile2Path_);
 		assertText(result, 3, " ");
-		assertEmoticon(result, 4, smile1_, smile1Path_);
+		assertHighlight(result, 4, "trigger");
 		assertText(result, 5, " ");
-		assertURL(result, 6, "http://wonderland.lit/blah");
+		assertEmoticon(result, 6, smile1_, smile1Path_);
 		assertText(result, 7, " ");
-		assertURL(result, 8, "http://denmark.lit");
-		assertText(result, 9, " boom boom");
+		assertURL(result, 8, "http://wonderland.lit/blah");
+		assertText(result, 9, " ");
+		assertURL(result, 10, "http://denmark.lit");
+		assertText(result, 11, " boom boom");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false));
+		result = testling.parseMessageBody("testtriggermessage");
+		assertText(result, 0, "test");
+		assertHighlight(result, 1, "trigger");
+		assertText(result, 2, "message");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, true));
+		result = testling.parseMessageBody("testtriggermessage");
+		assertText(result, 0, "testtriggermessage");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", true, false));
+		result = testling.parseMessageBody("TrIgGeR");
+		assertText(result, 0, "TrIgGeR");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false));
+		result = testling.parseMessageBody("TrIgGeR");
+		assertHighlight(result, 0, "TrIgGeR");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false));
+		result = testling.parseMessageBody("partialTrIgGeRmatch");
+		assertText(result, 0, "partial");
+		assertHighlight(result, 1, "TrIgGeR");
+		assertText(result, 2, "match");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false)));
+		result = testling.parseMessageBody("zero one two three");
+		assertText(result, 0, "zero ");
+		assertHighlight(result, 1, "one");
+		assertText(result, 2, " two ");
+		assertHighlight(result, 3, "three");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false)));
+		result = testling.parseMessageBody("zero oNe two tHrEe");
+		assertText(result, 0, "zero ");
+		assertHighlight(result, 1, "oNe");
+		assertText(result, 2, " two ");
+		assertHighlight(result, 3, "tHrEe");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", true, false)));
+		result = testling.parseMessageBody("zero oNe two tHrEe");
+		assertText(result, 0, "zero ");
+		assertHighlight(result, 1, "oNe");
+		assertText(result, 2, " two tHrEe");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", true, false)));
+		result = testling.parseMessageBody("zero oNe two tHrEe");
+		assertText(result, 0, "zero oNe two tHrEe");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false)));
+		result = testling.parseMessageBody("zeroonetwothree");
+		assertText(result, 0, "zero");
+		assertHighlight(result, 1, "one");
+		assertText(result, 2, "two");
+		assertHighlight(result, 3, "three");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", false, false)));
+		result = testling.parseMessageBody("zeroOnEtwoThReE");
+		assertText(result, 0, "zeroOnEtwo");
+		assertHighlight(result, 1, "ThReE");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, false)));
+		result = testling.parseMessageBody("zeroonetwothree");
+		assertText(result, 0, "zeroonetwo");
+		assertHighlight(result, 1, "three");
+
+		testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, true)));
+		result = testling.parseMessageBody("zeroonetwothree");
+		assertText(result, 0, "zeroonetwothree");
 	}
 
 	void testOneEmoticon() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody(" :) ");
 		assertText(result, 0, " ");
 		assertEmoticon(result, 1, smile1_, smile1Path_);
@@ -78,26 +186,26 @@ public:
 
 
 	void testBareEmoticon() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody(":)");
 		assertEmoticon(result, 0, smile1_, smile1Path_);
 	}
 
 	void testHiddenEmoticon() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a");
 		assertText(result, 0, "b:)a");
 	}
 
 	void testEndlineEmoticon() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody("Lazy:)");
 		assertText(result, 0, "Lazy");
 		assertEmoticon(result, 1, smile1_, smile1Path_);
 	}
 
 	void testBoundedEmoticons() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody(":)Lazy:(");
 		assertEmoticon(result, 0, smile1_, smile1Path_);
 		assertText(result, 1, "Lazy");
@@ -105,14 +213,13 @@ public:
 	}
 
 	void testEmoticonParenthesis() {
-		ChatMessageParser testling(emoticons_);
+		ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>());
 		ChatWindow::ChatMessage result = testling.parseMessageBody("(Like this :))");
 		assertText(result, 0, "(Like this ");
 		assertEmoticon(result, 1, smile1_, smile1Path_);
 		assertText(result, 2, ")");
 	}
 
-
 private:
 	std::map<std::string, std::string> emoticons_;
 	std::string smile1_;
@@ -122,4 +229,3 @@ private:
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(ChatMessageParserTest);
-
diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
index 7268878..bb22e43 100644
--- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
+++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
+ * Copyright (c) 2010-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -81,7 +81,7 @@ public:
 		highlightManager_ = new HighlightManager(settings_);
 		muc_ = boost::make_shared<MockMUC>(mucJID_);
 		mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_);
-		chatMessageParser_ = new ChatMessageParser(std::map<std::string, std::string>());
+		chatMessageParser_ = boost::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getRules(), true);
 		vcardStorage_ = new VCardMemoryStorage(crypto_.get());
 		vcardManager_ = new VCardManager(self_, iqRouter_, vcardStorage_);
 		controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_, highlightManager_, chatMessageParser_, false, NULL, vcardManager_);
@@ -105,7 +105,6 @@ public:
 		delete iqChannel_;
 		delete mucRegistry_;
 		delete avatarManager_;
-		delete chatMessageParser_;
 	}
 
 	void finishJoin() {
@@ -429,7 +428,7 @@ private:
 	DummyEntityCapsProvider* entityCapsProvider_;
 	DummySettingsProvider* settings_;
 	HighlightManager* highlightManager_;
-	ChatMessageParser* chatMessageParser_;
+	boost::shared_ptr<ChatMessageParser> chatMessageParser_;
 	boost::shared_ptr<CryptoProvider> crypto_;
 	VCardManager* vcardManager_;
 	VCardMemoryStorage* vcardStorage_;
diff --git a/Swift/Controllers/HighlightAction.h b/Swift/Controllers/HighlightAction.h
index bfbed74..de1f201 100644
--- a/Swift/Controllers/HighlightAction.h
+++ b/Swift/Controllers/HighlightAction.h
@@ -4,10 +4,19 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #pragma once
 
 #include <string>
 
+#include <boost/archive/text_oarchive.hpp>
+#include <boost/archive/text_iarchive.hpp>
+
 namespace Swift {
 
 	class HighlightRule;
@@ -19,21 +28,33 @@ namespace Swift {
 			bool highlightText() const { return highlightText_; }
 			void setHighlightText(bool highlightText);
 
+			/**
+			* Gets the foreground highlight color. If the string is empty, assume a default color.
+			*/
 			const std::string& getTextColor() const { return textColor_; }
 			void setTextColor(const std::string& textColor) { textColor_ = textColor; }
 
+			/**
+			* Gets the background highlight color. If the string is empty, assume a default color.
+			*/
 			const std::string& getTextBackground() const { return textBackground_; }
 			void setTextBackground(const std::string& textBackground) { textBackground_ = textBackground; }
 
 			bool playSound() const { return playSound_; }
 			void setPlaySound(bool playSound);
 
+			/**
+			* Gets the sound filename. If the string is empty, assume a default sound file.
+			*/
 			const std::string& getSoundFile() const { return soundFile_; }
 			void setSoundFile(const std::string& soundFile) { soundFile_ = soundFile; }
 
 			bool isEmpty() const { return !highlightText_ && !playSound_; }
 
 		private:
+			friend class boost::serialization::access;
+			template<class Archive> void serialize(Archive & ar, const unsigned int version);
+
 			bool highlightText_;
 			std::string textColor_;
 			std::string textBackground_;
@@ -42,4 +63,14 @@ namespace Swift {
 			std::string soundFile_;
 	};
 
+	template<class Archive>
+	void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/)
+	{
+		ar & highlightText_;
+		ar & textColor_;
+		ar & textBackground_;
+		ar & playSound_;
+		ar & soundFile_;
+	}
+
 }
diff --git a/Swift/Controllers/HighlightEditorController.cpp b/Swift/Controllers/HighlightEditorController.cpp
index 899e4bb..38007f0 100644
--- a/Swift/Controllers/HighlightEditorController.cpp
+++ b/Swift/Controllers/HighlightEditorController.cpp
@@ -4,36 +4,52 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #include <boost/bind.hpp>
 
 #include <Swift/Controllers/HighlightEditorController.h>
-#include <Swift/Controllers/UIInterfaces/HighlightEditorWidget.h>
-#include <Swift/Controllers/UIInterfaces/HighlightEditorWidgetFactory.h>
 #include <Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h>
 #include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h>
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h>
+#include <Swift/Controllers/ContactSuggester.h>
 
 namespace Swift {
 
-HighlightEditorController::HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWidgetFactory* highlightEditorWidgetFactory, HighlightManager* highlightManager) : highlightEditorWidgetFactory_(highlightEditorWidgetFactory), highlightEditorWidget_(NULL), highlightManager_(highlightManager)
+HighlightEditorController::HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager)
+: highlightEditorWindowFactory_(highlightEditorWindowFactory), highlightEditorWindow_(NULL), highlightManager_(highlightManager), contactSuggester_(0)
 {
 	uiEventStream->onUIEvent.connect(boost::bind(&HighlightEditorController::handleUIEvent, this, _1));
 }
 
 HighlightEditorController::~HighlightEditorController()
 {
-	delete highlightEditorWidget_;
-	highlightEditorWidget_ = NULL;
+	delete highlightEditorWindow_;
+	highlightEditorWindow_ = NULL;
 }
 
 void HighlightEditorController::handleUIEvent(boost::shared_ptr<UIEvent> rawEvent)
 {
 	boost::shared_ptr<RequestHighlightEditorUIEvent> event = boost::dynamic_pointer_cast<RequestHighlightEditorUIEvent>(rawEvent);
 	if (event) {
-		if (!highlightEditorWidget_) {
-			highlightEditorWidget_ = highlightEditorWidgetFactory_->createHighlightEditorWidget();
-			highlightEditorWidget_->setHighlightManager(highlightManager_);
+		if (!highlightEditorWindow_) {
+			highlightEditorWindow_ = highlightEditorWindowFactory_->createHighlightEditorWindow();
+			highlightEditorWindow_->setHighlightManager(highlightManager_);
+			highlightEditorWindow_->onContactSuggestionsRequested.connect(boost::bind(&HighlightEditorController::handleContactSuggestionsRequested, this, _1));
 		}
-		highlightEditorWidget_->show();
+		highlightEditorWindow_->show();
+	}
+}
+
+void HighlightEditorController::handleContactSuggestionsRequested(const std::string& text)
+{
+	if (contactSuggester_) {
+		highlightEditorWindow_->setContactSuggestions(contactSuggester_->getSuggestions(text));
 	}
 }
 
diff --git a/Swift/Controllers/HighlightEditorController.h b/Swift/Controllers/HighlightEditorController.h
index 3868251..54322e2 100644
--- a/Swift/Controllers/HighlightEditorController.h
+++ b/Swift/Controllers/HighlightEditorController.h
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #pragma once
 
 #include <boost/shared_ptr.hpp>
@@ -14,25 +20,29 @@ namespace Swift {
 
 	class UIEventStream;
 
-	class HighlightEditorWidgetFactory;
-	class HighlightEditorWidget;
+	class HighlightEditorWindowFactory;
+	class HighlightEditorWindow;
 
 	class HighlightManager;
+	class ContactSuggester;
 
 	class HighlightEditorController {
 		public:
-			HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWidgetFactory* highlightEditorWidgetFactory, HighlightManager* highlightManager);
+			HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWindowFactory* highlightEditorWindowFactory, HighlightManager* highlightManager);
 			~HighlightEditorController();
 
 			HighlightManager* getHighlightManager() const { return highlightManager_; }
+			void setContactSuggester(ContactSuggester *suggester) { contactSuggester_ = suggester; }
 
 		private:
 			void handleUIEvent(boost::shared_ptr<UIEvent> event);
+			void handleContactSuggestionsRequested(const std::string& text);
 
 		private:
-			HighlightEditorWidgetFactory* highlightEditorWidgetFactory_;
-			HighlightEditorWidget* highlightEditorWidget_;
+			HighlightEditorWindowFactory* highlightEditorWindowFactory_;
+			HighlightEditorWindow* highlightEditorWindow_;
 			HighlightManager* highlightManager_;
+			ContactSuggester* contactSuggester_;
 	};
 
 }
diff --git a/Swift/Controllers/HighlightManager.cpp b/Swift/Controllers/HighlightManager.cpp
index 7ab578e..eac562f 100644
--- a/Swift/Controllers/HighlightManager.cpp
+++ b/Swift/Controllers/HighlightManager.cpp
@@ -4,12 +4,21 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #include <cassert>
 
 #include <boost/algorithm/string.hpp>
 #include <boost/regex.hpp>
 #include <boost/bind.hpp>
 #include <boost/numeric/conversion/cast.hpp>
+#include <boost/serialization/vector.hpp>
+#include <boost/archive/text_oarchive.hpp>
+#include <boost/archive/text_iarchive.hpp>
 
 #include <Swiften/Base/foreach.h>
 #include <Swift/Controllers/HighlightManager.h>
@@ -40,6 +49,7 @@ HighlightManager::HighlightManager(SettingsProvider* settings)
 	: settings_(settings)
 	, storingSettings_(false)
 {
+	rules_ = boost::make_shared<HighlightRulesList>();
 	loadSettings();
 	settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1));
 }
@@ -51,40 +61,12 @@ void HighlightManager::handleSettingChanged(const std::string& settingPath)
 	}
 }
 
-void HighlightManager::loadSettings()
-{
-	std::string highlightRules = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES);
-	if (highlightRules == "@") {
-		rules_ = getDefaultRules();
-	} else {
-		rules_ = rulesFromString(highlightRules);
-	}
-}
-
 std::string HighlightManager::rulesToString() const
 {
-	std::string s;
-	foreach (HighlightRule r, rules_) {
-		s += r.toString() + '\f';
-	}
-	if (s.size()) {
-		s.erase(s.end() - 1);
-	}
-	return s;
-}
-
-std::vector<HighlightRule> HighlightManager::rulesFromString(const std::string& rulesString)
-{
-	std::vector<HighlightRule> rules;
-	std::string s(rulesString);
-	typedef boost::split_iterator<std::string::iterator> split_iterator;
-	for (split_iterator it = boost::make_split_iterator(s, boost::first_finder("\f")); it != split_iterator(); ++it) {
-		HighlightRule r = HighlightRule::fromString(boost::copy_range<std::string>(*it));
-		if (!r.isEmpty()) {
-			rules.push_back(r);
-		}
-	}
-	return rules;
+	std::stringstream stream;
+	boost::archive::text_oarchive archive(stream);
+	archive << rules_->list_;
+	return stream.str();
 }
 
 std::vector<HighlightRule> HighlightManager::getDefaultRules()
@@ -97,38 +79,48 @@ std::vector<HighlightRule> HighlightManager::getDefaultRules()
 	return rules;
 }
 
-void HighlightManager::storeSettings()
-{
-	storingSettings_ = true;	// don't reload settings while saving
-	settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES, rulesToString());
-	storingSettings_ = false;
-}
-
 HighlightRule HighlightManager::getRule(int index) const
 {
-	assert(index >= 0 && static_cast<size_t>(index) < rules_.size());
-	return rules_[static_cast<size_t>(index)];
+	assert(index >= 0 && static_cast<size_t>(index) < rules_->getSize());
+	return rules_->getRule(static_cast<size_t>(index));
 }
 
 void HighlightManager::setRule(int index, const HighlightRule& rule)
 {
-	assert(index >= 0 && static_cast<size_t>(index) < rules_.size());
-	rules_[static_cast<size_t>(index)] = rule;
-	storeSettings();
+	assert(index >= 0 && static_cast<size_t>(index) < rules_->getSize());
+	rules_->list_[static_cast<size_t>(index)] = rule;
 }
 
 void HighlightManager::insertRule(int index, const HighlightRule& rule)
 {
-	assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) <= rules_.size());
-	rules_.insert(rules_.begin() + index, rule);
-	storeSettings();
+	assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) <= rules_->getSize());
+	rules_->list_.insert(rules_->list_.begin() + index, rule);
 }
 
 void HighlightManager::removeRule(int index)
 {
-	assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) < rules_.size());
-	rules_.erase(rules_.begin() + index);
-	storeSettings();
+	assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) < rules_->getSize());
+	rules_->list_.erase(rules_->list_.begin() + index);
+}
+
+void HighlightManager::storeSettings()
+{
+	storingSettings_ = true;	// don't reload settings while saving
+	settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES, rulesToString());
+	storingSettings_ = false;
+}
+
+void HighlightManager::loadSettings()
+{
+	std::string rulesString = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES);
+	std::stringstream stream;
+	stream << rulesString;
+	try {
+		boost::archive::text_iarchive archive(stream);
+		archive >> rules_->list_;
+	} catch (boost::archive::archive_exception&) {
+		rules_->list_ = getDefaultRules();
+	}
 }
 
 Highlighter* HighlightManager::createHighlighter()
diff --git a/Swift/Controllers/HighlightManager.h b/Swift/Controllers/HighlightManager.h
index d195d05..3da72eb 100644
--- a/Swift/Controllers/HighlightManager.h
+++ b/Swift/Controllers/HighlightManager.h
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #pragma once
 
 #include <vector>
@@ -19,15 +25,32 @@ namespace Swift {
 
 	class HighlightManager {
 		public:
+
+			class HighlightRulesList {
+			public:
+				friend class HighlightManager;
+				size_t getSize() const { return list_.size(); }
+				const HighlightRule& getRule(const size_t index) const { return list_[index]; }
+				void addRule(const HighlightRule &rule) { list_.push_back(rule); }
+				void combineRules(const HighlightRulesList &rhs) {
+					list_.insert(list_.end(), rhs.list_.begin(), rhs.list_.end());
+				}
+			private:
+				std::vector<HighlightRule> list_;
+			};
+
 			HighlightManager(SettingsProvider* settings);
 
 			Highlighter* createHighlighter();
 
-			const std::vector<HighlightRule>& getRules() const { return rules_; }
+			boost::shared_ptr<const HighlightManager::HighlightRulesList> getRules() const { return rules_; }
+
 			HighlightRule getRule(int index) const;
 			void setRule(int index, const HighlightRule& rule);
 			void insertRule(int index, const HighlightRule& rule);
 			void removeRule(int index);
+			void storeSettings();
+			void loadSettings();
 
 			boost::signal<void (const HighlightAction&)> onHighlight;
 
@@ -35,15 +58,14 @@ namespace Swift {
 			void handleSettingChanged(const std::string& settingPath);
 
 			std::string rulesToString() const;
-			static std::vector<HighlightRule> rulesFromString(const std::string&);
 			static std::vector<HighlightRule> getDefaultRules();
 
 			SettingsProvider* settings_;
 			bool storingSettings_;
-			void storeSettings();
-			void loadSettings();
 
-			std::vector<HighlightRule> rules_;
+			boost::shared_ptr<HighlightManager::HighlightRulesList> rules_;
 	};
 
+	typedef boost::shared_ptr<const HighlightManager::HighlightRulesList> HighlightRulesListPtr;
+
 }
diff --git a/Swift/Controllers/HighlightRule.cpp b/Swift/Controllers/HighlightRule.cpp
index 9ca7d86..f1a5235 100644
--- a/Swift/Controllers/HighlightRule.cpp
+++ b/Swift/Controllers/HighlightRule.cpp
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #include <algorithm>
 #include <boost/algorithm/string.hpp>
 #include <boost/lambda/lambda.hpp>
@@ -56,57 +62,6 @@ bool HighlightRule::boolFromString(const std::string& s)
 	return s == "1";
 }
 
-std::string HighlightRule::toString() const
-{
-	std::vector<std::string> v;
-	v.push_back(boost::join(senders_, "\t"));
-	v.push_back(boost::join(keywords_, "\t"));
-	v.push_back(boolToString(nickIsKeyword_));
-	v.push_back(boolToString(matchChat_));
-	v.push_back(boolToString(matchMUC_));
-	v.push_back(boolToString(matchCase_));
-	v.push_back(boolToString(matchWholeWords_));
-	v.push_back(boolToString(action_.highlightText()));
-	v.push_back(action_.getTextColor());
-	v.push_back(action_.getTextBackground());
-	v.push_back(boolToString(action_.playSound()));
-	v.push_back(action_.getSoundFile());
-	return boost::join(v, "\n");
-}
-
-HighlightRule HighlightRule::fromString(const std::string& s)
-{
-	std::vector<std::string> v;
-	boost::split(v, s, boost::is_any_of("\n"));
-
-	HighlightRule r;
-	size_t i = 0;
-	try {
-		boost::split(r.senders_, v.at(i++), boost::is_any_of("\t"));
-		r.senders_.erase(std::remove_if(r.senders_.begin(), r.senders_.end(), boost::lambda::_1 == ""), r.senders_.end());
-		boost::split(r.keywords_, v.at(i++), boost::is_any_of("\t"));
-		r.keywords_.erase(std::remove_if(r.keywords_.begin(), r.keywords_.end(), boost::lambda::_1 == ""), r.keywords_.end());
-		r.nickIsKeyword_ = boolFromString(v.at(i++));
-		r.matchChat_ = boolFromString(v.at(i++));
-		r.matchMUC_ = boolFromString(v.at(i++));
-		r.matchCase_ = boolFromString(v.at(i++));
-		r.matchWholeWords_ = boolFromString(v.at(i++));
-		r.action_.setHighlightText(boolFromString(v.at(i++)));
-		r.action_.setTextColor(v.at(i++));
-		r.action_.setTextBackground(v.at(i++));
-		r.action_.setPlaySound(boolFromString(v.at(i++)));
-		r.action_.setSoundFile(v.at(i++));
-	}
-	catch (std::out_of_range) {
-		// this may happen if more properties are added to HighlightRule object, etc...
-		// in such case, default values (set by default constructor) will be used
-	}
-
-	r.updateRegex();
-
-	return r;
-}
-
 bool HighlightRule::isMatch(const std::string& body, const std::string& sender, const std::string& nick, MessageType messageType) const
 {
 	if ((messageType == HighlightRule::ChatMessage && matchChat_) || (messageType == HighlightRule::MUCMessage && matchMUC_)) {
@@ -114,13 +69,6 @@ bool HighlightRule::isMatch(const std::string& body, const std::string& sender,
 		bool matchesKeyword = keywords_.empty() && (nick.empty() || !nickIsKeyword_);
 		bool matchesSender = senders_.empty();
 
-		foreach (const boost::regex & rx, keywordRegex_) {
-			if (boost::regex_search(body, rx)) {
-				matchesKeyword = true;
-				break;
-			}
-		}
-
 		if (!matchesKeyword && nickIsKeyword_ && !nick.empty()) {
 			if (boost::regex_search(body, regexFromString(nick))) {
 				matchesKeyword = true;
diff --git a/Swift/Controllers/HighlightRule.h b/Swift/Controllers/HighlightRule.h
index 1abfa5a..ae1a3d3 100644
--- a/Swift/Controllers/HighlightRule.h
+++ b/Swift/Controllers/HighlightRule.h
@@ -4,12 +4,20 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #pragma once
 
 #include <vector>
 #include <string>
 
 #include <boost/regex.hpp>
+#include <boost/archive/text_oarchive.hpp>
+#include <boost/archive/text_iarchive.hpp>
 
 #include <Swift/Controllers/HighlightAction.h>
 
@@ -26,14 +34,13 @@ namespace Swift {
 			const HighlightAction& getAction() const { return action_; }
 			HighlightAction& getAction() { return action_; }
 
-			static HighlightRule fromString(const std::string&);
-			std::string toString() const;
-
 			const std::vector<std::string>& getSenders() const { return senders_; }
 			void setSenders(const std::vector<std::string>&);
+			const std::vector<boost::regex>& getSenderRegex() const { return senderRegex_; }
 
 			const std::vector<std::string>& getKeywords() const { return keywords_; }
 			void setKeywords(const std::vector<std::string>&);
+			const std::vector<boost::regex>& getKeywordRegex() const { return keywordRegex_; }
 
 			bool getNickIsKeyword() const { return nickIsKeyword_; }
 			void setNickIsKeyword(bool);
@@ -53,6 +60,9 @@ namespace Swift {
 			bool isEmpty() const;
 
 		private:
+			friend class boost::serialization::access;
+			template<class Archive> void serialize(Archive & ar, const unsigned int version);
+
 			static std::string boolToString(bool);
 			static bool boolFromString(const std::string&);
 
@@ -74,4 +84,18 @@ namespace Swift {
 			HighlightAction action_;
 	};
 
+	template<class Archive>
+	void HighlightRule::serialize(Archive& ar, const unsigned int /*version*/)
+	{
+		ar & senders_;
+		ar & keywords_;
+		ar & nickIsKeyword_;
+		ar & matchChat_;
+		ar & matchMUC_;
+		ar & matchCase_;
+		ar & matchWholeWords_;
+		ar & action_;
+		updateRegex();
+	}
+
 }
diff --git a/Swift/Controllers/Highlighter.cpp b/Swift/Controllers/Highlighter.cpp
index 754641a..efeeb6b 100644
--- a/Swift/Controllers/Highlighter.cpp
+++ b/Swift/Controllers/Highlighter.cpp
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #include <Swiften/Base/foreach.h>
 #include <Swift/Controllers/Highlighter.h>
 #include <Swift/Controllers/HighlightManager.h>
@@ -24,9 +30,11 @@ void Highlighter::setMode(Mode mode)
 
 HighlightAction Highlighter::findAction(const std::string& body, const std::string& sender) const
 {
-	foreach (const HighlightRule & r, manager_->getRules()) {
-		if (r.isMatch(body, sender, nick_, messageType_)) {
-			return r.getAction();
+	HighlightRulesListPtr rules = manager_->getRules();
+	for (size_t i = 0; i < rules->getSize(); ++i) {
+		const HighlightRule& rule = rules->getRule(i);
+		if (rule.isMatch(body, sender, nick_, messageType_)) {
+			return rule.getAction();
 		}
 	}
 
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 79b7502..a16cbe7 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -1,11 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
- * Licensed under the GNU General Public License v3.
- * See Documentation/Licenses/GPLv3.txt for more information.
- */
-
-/*
- * Copyright (c) 2013 Remko Tronçon
+ * Copyright (c) 2010-2014 Kevin Smith and Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -369,6 +363,7 @@ void MainController::handleConnected() {
 		contactSuggesterWithoutRoster_->addContactProvider(chatsManager_);
 		contactSuggesterWithRoster_->addContactProvider(chatsManager_);
 		contactSuggesterWithRoster_->addContactProvider(contactsFromRosterProvider_);
+		highlightEditorController_->setContactSuggester(contactSuggesterWithoutRoster_);
 
 		client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1));
 		chatsManager_->setAvatarManager(client_->getAvatarManager());
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index 4c71268..5ebbdd3 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -62,6 +62,7 @@ if env["SCONS_STAGE"] == "build" :
 			"UIEvents/UIEvent.cpp",
 			"UIInterfaces/XMLConsoleWidget.cpp",
 			"UIInterfaces/ChatListWindow.cpp",
+			"UIInterfaces/HighlightEditorWindow.cpp",
 			"PreviousStatusStore.cpp",
 			"ProfileSettingsProvider.cpp",
 			"Settings/SettingsProviderHierachy.cpp",
diff --git a/Swift/Controllers/UIInterfaces/HighlightEditorWindow.cpp b/Swift/Controllers/UIInterfaces/HighlightEditorWindow.cpp
new file mode 100644
index 0000000..f90903b
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/HighlightEditorWindow.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h>
+
+namespace Swift {
+
+HighlightEditorWindow::HighlightEditorWindow()
+{
+}
+
+HighlightEditorWindow::~HighlightEditorWindow()
+{
+}
+
+}
diff --git a/Swift/Controllers/UIInterfaces/HighlightEditorWindow.h b/Swift/Controllers/UIInterfaces/HighlightEditorWindow.h
new file mode 100644
index 0000000..83ae959
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/HighlightEditorWindow.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <vector>
+#include <Swiften/Base/boost_bsignals.h>
+#include <Swift/Controllers/Contact.h>
+
+namespace Swift {
+
+class HighlightManager;
+
+class HighlightEditorWindow {
+public:
+	HighlightEditorWindow();
+	virtual ~HighlightEditorWindow();
+	virtual void show() = 0;
+	virtual void setHighlightManager(HighlightManager *highlightManager) = 0;
+	virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions) = 0;
+
+public:
+	boost::signal<void (const std::string&)> onContactSuggestionsRequested;
+};
+
+}
diff --git a/Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h b/Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h
new file mode 100644
index 0000000..e0aaaef
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2012 Mateusz Piękos
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+namespace Swift {
+	class HighlightEditorWindow;
+
+	class HighlightEditorWindowFactory {
+	public :
+		virtual ~HighlightEditorWindowFactory() {}
+
+		virtual HighlightEditorWindow* createHighlightEditorWindow() = 0;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h
index 990dc98..54fa7ce 100644
--- a/Swift/Controllers/UIInterfaces/UIFactory.h
+++ b/Swift/Controllers/UIInterfaces/UIFactory.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010 Remko Tronçon
+ * Copyright (c) 2010-2014 Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -21,7 +21,7 @@
 #include <Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/FileTransferListWidgetFactory.h>
 #include <Swift/Controllers/UIInterfaces/WhiteboardWindowFactory.h>
-#include <Swift/Controllers/UIInterfaces/HighlightEditorWidgetFactory.h>
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/BlockListEditorWidgetFactory.h>
 
 namespace Swift {
@@ -41,7 +41,7 @@ namespace Swift {
 			public AdHocCommandWindowFactory,
 			public FileTransferListWidgetFactory,
 			public WhiteboardWindowFactory,
-			public HighlightEditorWidgetFactory,
+			public HighlightEditorWindowFactory,
 			public BlockListEditorWidgetFactory {
 		public:
 			virtual ~UIFactory() {}
diff --git a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp
index ec81227..c988b8d 100644
--- a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp
+++ b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp
@@ -4,6 +4,12 @@
  * See Documentation/Licenses/BSD-simplified.txt for more information.
  */
 
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
 #include <vector>
 #include <string>
 
@@ -34,7 +40,7 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			keywords.push_back("keyword1");
 			keywords.push_back("KEYWORD2");
 
-			std::vector<std::string>senders;
+			std::vector<std::string> senders;
 			senders.push_back("sender1");
 			senders.push_back("SENDER2");
 
@@ -157,20 +163,20 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::MUCMessage), false);
 			CPPUNIT_ASSERT_EQUAL(keywordRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 
 			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "sender contains keyword1", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc keyword1 xyz", "from", "nick", HighlightRule::ChatMessage), true);
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abckeyword1xyz", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc keyword1 xyz", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abckeyword1xyz", "from", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("KEYword1", "from", "nick", HighlightRule::ChatMessage), true);
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc KEYword1 xyz", "from", "nick", HighlightRule::ChatMessage), true);
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abcKEYword1xyz", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("KEYword1", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc KEYword1 xyz", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abcKEYword1xyz", "from", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword2", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword2", "from", "nick", HighlightRule::ChatMessage), false);
 		}
 
 		void testNickKeyword() {
@@ -178,7 +184,7 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false);
 			CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 
 			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "sender contains nick", "nick", HighlightRule::ChatMessage), false);
 
@@ -232,7 +238,7 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false);
-			CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
 		}
 
 		void testWholeWords() {
@@ -240,14 +246,14 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false);
 
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), true);
 		}
 
@@ -256,8 +262,8 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true);
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("xkeyword1", "xsender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("xkeyword1", "xsender1", "nick", HighlightRule::ChatMessage), false);
 
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), true);
@@ -273,7 +279,7 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false);
 
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false);
 
@@ -290,7 +296,7 @@ class HighlightRuleTest : public CppUnit::TestFixture {
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
 
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
-			CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::MUCMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::MUCMessage), false);
 			CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::MUCMessage), true);
 		}
 
diff --git a/Swift/QtUI/QtHighlightEditor.cpp b/Swift/QtUI/QtHighlightEditor.cpp
new file mode 100644
index 0000000..3900cf9
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditor.cpp
@@ -0,0 +1,524 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <cassert>
+
+#include <boost/lexical_cast.hpp>
+
+#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h>
+#include <Swift/Controllers/HighlightManager.cpp>
+#include <Swift/QtUI/QtHighlightEditor.h>
+#include <Swift/QtUI/QtSwiftUtil.h>
+#include <Swift/QtUI/QtSettingsProvider.h>
+
+#include <QTreeWidgetItem>
+#include <QFileDialog>
+
+namespace Swift {
+
+QtHighlightEditor::QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent)
+	: QWidget(parent), settings_(settings), previousRow_(-1)
+{
+	ui_.setupUi(this);
+
+	connect(ui_.listWidget, SIGNAL(currentRowChanged(int)), SLOT(onCurrentRowChanged(int)));
+
+	connect(ui_.newButton, SIGNAL(clicked()), SLOT(onNewButtonClicked()));
+	connect(ui_.deleteButton, SIGNAL(clicked()), SLOT(onDeleteButtonClicked()));
+
+	connect(ui_.buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(onApplyButtonClick()));
+	connect(ui_.buttonBox->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), SLOT(onCancelButtonClick()));
+	connect(ui_.buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(onOkButtonClick()));
+
+	connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect()));
+	connect(ui_.defaultColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect()));
+	connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(colorCustomSelect()));
+
+	connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect()));
+	connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect()));
+	connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(soundCustomSelect()));
+
+	/* replace the static line-edit control with the roster autocompleter */
+	ui_.dummySenderName->setVisible(false);
+	jid_ = new QtSuggestingJIDInput(this, settings);
+	ui_.senderName->addWidget(jid_);
+	jid_->onUserSelected.connect(boost::bind(&QtHighlightEditor::handleOnUserSelected, this, _1));
+
+	/* handle autocomplete */
+	connect(jid_, SIGNAL(textEdited(QString)), SLOT(handleContactSuggestionRequested(QString)));
+
+	/* we need to be notified if any of the state changes so that we can update our textual rule description */
+	connect(ui_.chatRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.roomRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.nickIsKeyword, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.allMsgRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.senderRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(jid_, SIGNAL(textChanged(const QString&)), SLOT(widgetClick()));
+	connect(ui_.keywordRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.keyword, SIGNAL(textChanged(const QString&)), SLOT(widgetClick()));
+	connect(ui_.matchPartialWords, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.matchCase, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.defaultColorRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+	connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(widgetClick()));
+
+	/* allow selection of a custom sound file */
+	connect(ui_.soundFileButton, SIGNAL(clicked()), SLOT(selectSoundFile()));
+
+	/* if these are not needed, then they should be removed */
+	ui_.moveUpButton->setVisible(false);
+	ui_.moveDownButton->setVisible(false);
+
+	setWindowTitle(tr("Highlight Rules"));
+}
+
+QtHighlightEditor::~QtHighlightEditor()
+{
+}
+
+std::string formatShortDescription(const HighlightRule &rule)
+{
+	const std::string chatOrRoom = (rule.getMatchChat() ? "chat" : "room");
+
+	std::vector<std::string> senders = rule.getSenders();
+	std::vector<std::string> keywords = rule.getKeywords();
+
+	if (senders.empty() && keywords.empty() && !rule.getNickIsKeyword()) {
+		return std::string("All ") + chatOrRoom + " messages.";
+	}
+
+	if (rule.getNickIsKeyword()) {
+		return std::string("All ") + chatOrRoom + " messages that mention my name.";
+	}
+
+	if (!senders.empty()) {
+		return std::string("All ") + chatOrRoom + " messages from " + senders[0] + ".";
+	}
+
+	if (!keywords.empty()) {
+		return std::string("All ") + chatOrRoom + " messages mentioning the keyword '" + keywords[0] + "'.";
+	}
+
+	return "Unknown Rule";
+}
+
+void QtHighlightEditor::show()
+{
+	highlightManager_->loadSettings();
+
+	populateList();
+
+	if (ui_.listWidget->count()) {
+		selectRow(0);
+	}
+
+	/* prepare default states */
+	widgetClick();
+
+	QWidget::show();
+	QWidget::activateWindow();
+}
+
+void QtHighlightEditor::setHighlightManager(HighlightManager* highlightManager)
+{
+	highlightManager_ = highlightManager;
+}
+
+void QtHighlightEditor::setContactSuggestions(const std::vector<Contact::ref>& suggestions)
+{
+	jid_->setSuggestions(suggestions);
+}
+
+void QtHighlightEditor::colorOtherSelect()
+{
+	ui_.foregroundColor->setEnabled(false);
+	ui_.backgroundColor->setEnabled(false);
+}
+
+void QtHighlightEditor::colorCustomSelect()
+{
+	ui_.foregroundColor->setEnabled(true);
+	ui_.backgroundColor->setEnabled(true);
+}
+
+void QtHighlightEditor::soundOtherSelect()
+{
+	ui_.soundFile->setEnabled(false);
+	ui_.soundFileButton->setEnabled(false);
+}
+
+void QtHighlightEditor::soundCustomSelect()
+{
+	ui_.soundFile->setEnabled(true);
+	ui_.soundFileButton->setEnabled(true);
+}
+
+void QtHighlightEditor::onNewButtonClicked()
+{
+	int row = getSelectedRow() + 1;
+	populateList();
+	HighlightRule newRule;
+	newRule.setMatchChat(true);
+	highlightManager_->insertRule(row, newRule);
+	QListWidgetItem *item = new QListWidgetItem();
+	item->setText(P2QSTRING(formatShortDescription(newRule)));
+	ui_.listWidget->insertItem(row, item);
+	selectRow(row);
+}
+
+void QtHighlightEditor::onDeleteButtonClicked()
+{
+	int selectedRow = getSelectedRow();
+	assert(selectedRow>=0 && selectedRow<ui_.listWidget->count());
+	delete ui_.listWidget->takeItem(selectedRow);
+	highlightManager_->removeRule(selectedRow);
+
+	if (!ui_.listWidget->count()) {
+		disableDialog();
+		ui_.deleteButton->setEnabled(false);
+	} else {
+		if (selectedRow == ui_.listWidget->count()) {
+			selectRow(ui_.listWidget->count() - 1);
+		} else {
+			selectRow(selectedRow);
+		}
+	}
+}
+
+void QtHighlightEditor::onCurrentRowChanged(int currentRow)
+{
+	ui_.deleteButton->setEnabled(currentRow != -1);
+	ui_.moveUpButton->setEnabled(currentRow != -1 && currentRow != 0);
+	ui_.moveDownButton->setEnabled(currentRow != -1 && currentRow != (ui_.listWidget->count()-1));
+
+	if (previousRow_ != -1) {
+		if (ui_.listWidget->count() > previousRow_) {
+			highlightManager_->setRule(previousRow_, ruleFromDialog());
+		}
+	}
+
+	if (currentRow != -1) {
+		HighlightRule rule = highlightManager_->getRule(currentRow);
+		ruleToDialog(rule);
+		if (ui_.listWidget->currentItem()) {
+			ui_.listWidget->currentItem()->setText(P2QSTRING(formatShortDescription(rule)));
+		}
+	}
+
+	/* grey the dialog if we have nothing selected */
+	if (currentRow == -1) {
+		disableDialog();
+	}
+
+	previousRow_ = currentRow;
+}
+
+void QtHighlightEditor::onApplyButtonClick()
+{
+	selectRow(getSelectedRow()); /* force save */
+	highlightManager_->storeSettings();
+}
+
+void QtHighlightEditor::onCancelButtonClick()
+{
+	close();
+}
+
+void QtHighlightEditor::onOkButtonClick()
+{
+	onApplyButtonClick();
+	close();
+}
+
+void QtHighlightEditor::setChildWidgetStates()
+{
+	/* disable appropriate radio button child widgets */
+
+	if (ui_.chatRadio->isChecked()) {
+		if (ui_.nickIsKeyword->isChecked()) {
+			/* switch to another choice before we disable this button */
+			ui_.allMsgRadio->setChecked(true);
+		}
+		ui_.nickIsKeyword->setEnabled(false);
+	} else if (ui_.roomRadio->isChecked()) {
+		ui_.nickIsKeyword->setEnabled(true);
+	} else { /* chats and rooms */
+		ui_.nickIsKeyword->setEnabled(true);
+	}
+
+	if (ui_.senderRadio->isChecked()) {
+		jid_->setEnabled(true);
+	} else {
+		jid_->setEnabled(false);
+	}
+
+	if (ui_.keywordRadio->isChecked()) {
+		ui_.keyword->setEnabled(true);
+		ui_.matchPartialWords->setEnabled(true);
+		ui_.matchCase->setEnabled(true);
+	} else {
+		ui_.keyword->setEnabled(false);
+		ui_.matchPartialWords->setEnabled(false);
+		ui_.matchCase->setEnabled(false);
+	}
+
+	if (ui_.chatRadio->isChecked()) {
+		ui_.allMsgRadio->setText(tr("Apply to all chat messages"));
+	} else {
+		ui_.allMsgRadio->setText(tr("Apply to all room messages"));
+	}
+}
+
+void QtHighlightEditor::widgetClick()
+{
+	setChildWidgetStates();
+
+	HighlightRule rule = ruleFromDialog();
+
+	if (ui_.listWidget->currentItem()) {
+		ui_.listWidget->currentItem()->setText(P2QSTRING(formatShortDescription(rule)));
+	}
+}
+
+void QtHighlightEditor::disableDialog()
+{
+	ui_.chatRadio->setEnabled(false);
+	ui_.roomRadio->setEnabled(false);
+	ui_.allMsgRadio->setEnabled(false);
+	ui_.nickIsKeyword->setEnabled(false);
+	ui_.senderRadio->setEnabled(false);
+	ui_.dummySenderName->setEnabled(false);
+	ui_.keywordRadio->setEnabled(false);
+	ui_.keyword->setEnabled(false);
+	ui_.matchPartialWords->setEnabled(false);
+	ui_.matchCase->setEnabled(false);
+	ui_.noColorRadio->setEnabled(false);
+	ui_.defaultColorRadio->setEnabled(false);
+	ui_.customColorRadio->setEnabled(false);
+	ui_.foregroundColor->setEnabled(false);
+	ui_.backgroundColor->setEnabled(false);
+	ui_.noSoundRadio->setEnabled(false);
+	ui_.defaultSoundRadio->setEnabled(false);
+	ui_.customSoundRadio->setEnabled(false);
+	ui_.soundFile->setEnabled(false);
+	ui_.soundFileButton->setEnabled(false);
+}
+
+void QtHighlightEditor::handleContactSuggestionRequested(const QString& text)
+{
+	std::string stdText = Q2PSTRING(text);
+	onContactSuggestionsRequested(stdText);
+}
+
+void QtHighlightEditor::selectSoundFile()
+{
+	QString path = QFileDialog::getOpenFileName(this, tr("Select sound file..."), QString(), "Sounds (*.wav)");
+	ui_.soundFile->setText(path);
+}
+
+void QtHighlightEditor::handleOnUserSelected(const JID& jid) {
+	/* this might seem like it should be standard behaviour for the suggesting input box, but is not desirable in all cases */
+	jid_->setText(P2QSTRING(jid.toString()));
+}
+
+void QtHighlightEditor::populateList()
+{
+	previousRow_ = -1;
+	ui_.listWidget->clear();
+	HighlightRulesListPtr rules = highlightManager_->getRules();
+	for (size_t i = 0; i < rules->getSize(); ++i) {
+		const HighlightRule& rule = rules->getRule(i);
+		QListWidgetItem *item = new QListWidgetItem();
+		item->setText(P2QSTRING(formatShortDescription(rule)));
+		ui_.listWidget->addItem(item);
+	}
+}
+
+void QtHighlightEditor::selectRow(int row)
+{
+	for (int i = 0; i < ui_.listWidget->count(); ++i) {
+		if (i == row) {
+			ui_.listWidget->item(i)->setSelected(true);
+			onCurrentRowChanged(i);
+		} else {
+			ui_.listWidget->item(i)->setSelected(false);
+		}
+	}
+	ui_.listWidget->setCurrentRow(row);
+}
+
+int QtHighlightEditor::getSelectedRow() const
+{
+	for (int i = 0; i < ui_.listWidget->count(); ++i) {
+		if (ui_.listWidget->item(i)->isSelected()) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+HighlightRule QtHighlightEditor::ruleFromDialog()
+{
+	HighlightRule rule;
+
+	if (ui_.chatRadio->isChecked()) {
+		rule.setMatchChat(true);
+		rule.setMatchMUC(false);
+	} else {
+		rule.setMatchChat(false);
+		rule.setMatchMUC(true);
+	}
+
+	if (ui_.senderRadio->isChecked()) {
+		QString senderName = jid_->text();
+		if (!senderName.isEmpty()) {
+			std::vector<std::string> senders;
+			senders.push_back(Q2PSTRING(senderName));
+			rule.setSenders(senders);
+		}
+	}
+
+	if (ui_.keywordRadio->isChecked()) {
+		QString keywordString = ui_.keyword->text();
+		if (!keywordString.isEmpty()) {
+			std::vector<std::string> keywords;
+			keywords.push_back(Q2PSTRING(keywordString));
+			rule.setKeywords(keywords);
+		}
+	}
+
+	rule.setNickIsKeyword(ui_.nickIsKeyword->isChecked());
+	rule.setMatchWholeWords(!ui_.matchPartialWords->isChecked());
+	rule.setMatchCase(ui_.matchCase->isChecked());
+
+	HighlightAction& action = rule.getAction();
+
+	if (ui_.noColorRadio->isChecked()) {
+		action.setHighlightText(false);
+		action.setTextColor("");
+		action.setTextBackground("");
+	} else if (ui_.defaultColorRadio->isChecked()) {
+		action.setHighlightText(true);
+		action.setTextColor("");
+		action.setTextBackground("");
+	} else {
+		action.setHighlightText(true);
+		action.setTextColor(Q2PSTRING(ui_.foregroundColor->getColor().name()));
+		action.setTextBackground(Q2PSTRING(ui_.backgroundColor->getColor().name()));
+	}
+
+	if (ui_.noSoundRadio->isChecked()) {
+		action.setPlaySound(false);
+	} else if (ui_.defaultSoundRadio->isChecked()) {
+		action.setPlaySound(true);
+		action.setSoundFile("");
+	} else {
+		action.setPlaySound(true);
+		action.setSoundFile(Q2PSTRING(ui_.soundFile->text()));
+	}
+
+	return rule;
+}
+
+void QtHighlightEditor::ruleToDialog(const HighlightRule& rule)
+{
+	ui_.chatRadio->setEnabled(true);
+	ui_.roomRadio->setEnabled(true);
+
+	if (rule.getMatchMUC()) {
+		ui_.chatRadio->setChecked(false);
+		ui_.roomRadio->setChecked(true);
+	} else {
+		ui_.chatRadio->setChecked(true);
+		ui_.roomRadio->setChecked(false);
+	}
+
+	ui_.allMsgRadio->setEnabled(true);
+	ui_.allMsgRadio->setChecked(true); /* this is the default radio button */
+	jid_->setText("");
+	ui_.keyword->setText("");
+	ui_.matchPartialWords->setChecked(false);
+	ui_.matchCase->setChecked(false);
+
+	ui_.nickIsKeyword->setEnabled(true);
+	if (rule.getNickIsKeyword()) {
+		ui_.nickIsKeyword->setChecked(true);
+	}
+
+	ui_.senderRadio->setEnabled(true);
+	std::vector<std::string> senders = rule.getSenders();
+	if (!senders.empty()) {
+		ui_.senderRadio->setChecked(true);
+		jid_->setText(P2QSTRING(senders[0]));
+	}
+
+	ui_.keywordRadio->setEnabled(true);
+	std::vector<std::string> keywords = rule.getKeywords();
+	if (!keywords.empty()) {
+		ui_.keywordRadio->setChecked(true);
+		ui_.keyword->setText(P2QSTRING(keywords[0]));
+		ui_.matchPartialWords->setChecked(!rule.getMatchWholeWords());
+		ui_.matchCase->setChecked(rule.getMatchCase());
+	}
+
+	const HighlightAction& action = rule.getAction();
+
+	ui_.noColorRadio->setEnabled(true);
+	ui_.defaultColorRadio->setEnabled(true);
+	ui_.customColorRadio->setEnabled(true);
+	if (action.highlightText()) {
+		if (action.getTextColor().empty() && action.getTextBackground().empty()) {
+			ui_.defaultColorRadio->setChecked(true);
+			ui_.foregroundColor->setEnabled(false);
+			ui_.backgroundColor->setEnabled(false);
+		} else {
+			ui_.foregroundColor->setEnabled(true);
+			ui_.backgroundColor->setEnabled(true);
+			QColor foregroundColor(P2QSTRING(action.getTextColor()));
+			ui_.foregroundColor->setColor(foregroundColor);
+			QColor backgroundColor(P2QSTRING(action.getTextBackground()));
+			ui_.backgroundColor->setColor(backgroundColor);
+			ui_.customColorRadio->setChecked(true);
+		}
+	} else {
+		ui_.noColorRadio->setChecked(true);
+		ui_.foregroundColor->setEnabled(false);
+		ui_.backgroundColor->setEnabled(false);
+	}
+
+	ui_.noSoundRadio->setEnabled(true);
+	ui_.defaultSoundRadio->setEnabled(true);
+	ui_.customSoundRadio->setEnabled(true);
+	ui_.soundFile->setText("");
+	ui_.soundFile->setEnabled(false);
+	ui_.soundFileButton->setEnabled(false);
+	if (action.playSound()) {
+		if (action.getSoundFile().empty()) {
+			ui_.defaultSoundRadio->setChecked(true);
+		} else {
+			ui_.customSoundRadio->setChecked(true);
+			ui_.soundFile->setText(P2QSTRING(action.getSoundFile()));
+			ui_.soundFile->setEnabled(true);
+			ui_.soundFileButton->setEnabled(true);
+		}
+	} else {
+		ui_.noSoundRadio->setChecked(true);
+	}
+
+	/* set radio button child option states */
+	setChildWidgetStates();
+}
+
+}
diff --git a/Swift/QtUI/QtHighlightEditor.h b/Swift/QtUI/QtHighlightEditor.h
new file mode 100644
index 0000000..c7db464
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditor.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+/*
+ * Copyright (c) 2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <Swift/Controllers/HighlightRule.h>
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h>
+#include <Swift/QtUI/ui_QtHighlightEditor.h>
+
+namespace Swift {
+
+	class QtSettingsProvider;
+	class QtSuggestingJIDInput;
+	class QtWebKitChatView;
+
+	class QtHighlightEditor : public QWidget, public HighlightEditorWindow {
+		Q_OBJECT
+
+		public:
+			QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent = NULL);
+			virtual ~QtHighlightEditor();
+
+			virtual void show();
+			virtual void setHighlightManager(HighlightManager* highlightManager);
+			virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions);
+
+		private slots:
+			void colorOtherSelect();
+			void colorCustomSelect();
+			void soundOtherSelect();
+			void soundCustomSelect();
+			void onNewButtonClicked();
+			void onDeleteButtonClicked();
+			void onCurrentRowChanged(int currentRow);
+			void onApplyButtonClick();
+			void onCancelButtonClick();
+			void onOkButtonClick();
+			void setChildWidgetStates();
+			void widgetClick();
+			void disableDialog();
+			void handleContactSuggestionRequested(const QString& text);
+			void selectSoundFile();
+
+		private:
+			void handleOnUserSelected(const JID& jid);
+			void populateList();
+			void updateChatPreview();
+			void selectRow(int row);
+			int getSelectedRow() const;
+			HighlightRule ruleFromDialog();
+			void ruleToDialog(const HighlightRule& rule);
+
+			Ui::QtHighlightEditor ui_;
+			QtSettingsProvider* settings_;
+			HighlightManager* highlightManager_;
+			QtSuggestingJIDInput* jid_;
+			int previousRow_;
+		};
+
+}
diff --git a/Swift/QtUI/QtHighlightEditor.ui b/Swift/QtUI/QtHighlightEditor.ui
new file mode 100644
index 0000000..09a7297
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditor.ui
@@ -0,0 +1,446 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtHighlightEditor</class>
+ <widget class="QWidget" name="QtHighlightEditor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>439</width>
+    <height>836</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>439</width>
+    <height>836</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QLabel" name="label_5">
+     <property name="text">
+      <string>Incoming messages are checked against the following rules. First rule that matches will be executed.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QListWidget" name="listWidget"/>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <spacer name="horizontalSpacer_8">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="newButton">
+       <property name="text">
+        <string>New Rule</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="deleteButton">
+       <property name="text">
+        <string>Remove Rule</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="moveUpButton">
+       <property name="text">
+        <string>Move Up</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="moveDownButton">
+       <property name="text">
+        <string>Move Down</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Line" name="line_3">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Apply Rule To</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QRadioButton" name="chatRadio">
+        <property name="text">
+         <string>Chats</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="roomRadio">
+        <property name="text">
+         <string>Rooms</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>246</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_6">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Rule Conditions</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QRadioButton" name="allMsgRadio">
+        <property name="text">
+         <string>Apply to all messages</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="nickIsKeyword">
+        <property name="text">
+         <string>Only messages mentioning me</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="senderRadio">
+        <property name="text">
+         <string>Messages from this sender:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="senderName">
+        <property name="sizeConstraint">
+         <enum>QLayout::SetMinimumSize</enum>
+        </property>
+        <item>
+         <widget class="QLineEdit" name="dummySenderName"/>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="keywordRadio">
+        <property name="text">
+         <string>Messages containing this keyword:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLineEdit" name="keyword"/>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="matchPartialWords">
+        <property name="text">
+         <string>Match keyword within longer words</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="matchCase">
+        <property name="text">
+         <string>Keyword is case sensitive</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_3">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Highlight Action</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_5">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_5">
+        <item>
+         <widget class="QRadioButton" name="noColorRadio">
+          <property name="text">
+           <string>No Highlight</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QRadioButton" name="defaultColorRadio">
+          <property name="text">
+           <string>Default Color</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QRadioButton" name="customColorRadio">
+          <property name="text">
+           <string>Custom Color</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_5">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_6">
+        <item>
+         <spacer name="horizontalSpacer_2">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="Swift::QtColorToolButton" name="foregroundColor">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>&amp;Text</string>
+          </property>
+          <property name="toolButtonStyle">
+           <enum>Qt::ToolButtonTextBesideIcon</enum>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="Swift::QtColorToolButton" name="backgroundColor">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>&amp;Background</string>
+          </property>
+          <property name="toolButtonStyle">
+           <enum>Qt::ToolButtonTextBesideIcon</enum>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_4">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="title">
+      <string>Sound Action</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_6">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_7">
+        <item>
+         <widget class="QRadioButton" name="noSoundRadio">
+          <property name="text">
+           <string>No Sound</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QRadioButton" name="defaultSoundRadio">
+          <property name="text">
+           <string>Default Sound</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QRadioButton" name="customSoundRadio">
+          <property name="text">
+           <string>Custom Sound</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_6">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_8">
+        <item>
+         <spacer name="horizontalSpacer_3">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QLineEdit" name="soundFile">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="readOnly">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QToolButton" name="soundFileButton">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>...</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="Line" name="line_2">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_9">
+     <item>
+      <spacer name="horizontalSpacer_4">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QDialogButtonBox" name="buttonBox">
+       <property name="standardButtons">
+        <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>Swift::QtColorToolButton</class>
+   <extends>QToolButton</extends>
+   <header>QtColorToolButton.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/Swift/QtUI/QtHighlightRuleWidget.cpp b/Swift/QtUI/QtHighlightRuleWidget.cpp
deleted file mode 100644
index 9c0df5e..0000000
--- a/Swift/QtUI/QtHighlightRuleWidget.cpp
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (c) 2012 Maciej Niedzielski
- * Licensed under the simplified BSD license.
- * See Documentation/Licenses/BSD-simplified.txt for more information.
- */
-
-#include <QDataWidgetMapper>
-#include <QStringListModel>
-#include <QFileDialog>
-
-#include <Swift/QtUI/QtHighlightRuleWidget.h>
-#include <Swift/QtUI/QtHighlightRulesItemModel.h>
-
-namespace Swift {
-
-QtHighlightRuleWidget::QtHighlightRuleWidget(QWidget* parent)
-	: QWidget(parent)
-{
-	ui_.setupUi(this);
-
-	QStringList applyToItems;
-	for (int i = 0; i < QtHighlightRulesItemModel::ApplyToEOL; ++i) {
-		applyToItems << QtHighlightRulesItemModel::getApplyToString(i);
-	}
-	QStringListModel * applyToModel = new QStringListModel(applyToItems, this);
-	ui_.applyTo->setModel(applyToModel);
-
-	connect(ui_.highlightText, SIGNAL(toggled(bool)), SLOT(onHighlightTextToggled(bool)));
-	connect(ui_.customColors, SIGNAL(toggled(bool)), SLOT(onCustomColorsToggled(bool)));
-	connect(ui_.playSound, SIGNAL(toggled(bool)), SLOT(onPlaySoundToggled(bool)));
-	connect(ui_.customSound, SIGNAL(toggled(bool)), SLOT(onCustomSoundToggled(bool)));
-	connect(ui_.soundFileButton, SIGNAL(clicked()), SLOT(onSoundFileButtonClicked()));
-
-	mapper_ = new QDataWidgetMapper(this);
-	hasValidIndex_ = false;
-	model_ = NULL;
-}
-
-QtHighlightRuleWidget::~QtHighlightRuleWidget()
-{
-}
-
-/** Widget does not gain ownership over the model */
-void QtHighlightRuleWidget::setModel(QtHighlightRulesItemModel* model)
-{
-	model_ = model;
-	mapper_->setModel(model_);
-}
-
-void QtHighlightRuleWidget::setActiveIndex(const QModelIndex& index)
-{
-	if (index.isValid()) {
-		if (!hasValidIndex_) {
-			mapper_->addMapping(ui_.applyTo, QtHighlightRulesItemModel::ApplyTo, "currentIndex");
-			mapper_->addMapping(ui_.senders, QtHighlightRulesItemModel::Sender, "plainText");
-			mapper_->addMapping(ui_.keywords, QtHighlightRulesItemModel::Keyword, "plainText");
-			mapper_->addMapping(ui_.nickIsKeyword, QtHighlightRulesItemModel::NickIsKeyword);
-			mapper_->addMapping(ui_.matchCase, QtHighlightRulesItemModel::MatchCase);
-			mapper_->addMapping(ui_.matchWholeWords, QtHighlightRulesItemModel::MatchWholeWords);
-			mapper_->addMapping(ui_.highlightText, QtHighlightRulesItemModel::HighlightText);
-			mapper_->addMapping(ui_.foreground, QtHighlightRulesItemModel::TextColor, "color");
-			mapper_->addMapping(ui_.background, QtHighlightRulesItemModel::TextBackground, "color");
-			mapper_->addMapping(ui_.playSound, QtHighlightRulesItemModel::PlaySound);
-			mapper_->addMapping(ui_.soundFile, QtHighlightRulesItemModel::SoundFile);
-		}
-		mapper_->setCurrentModelIndex(index);
-		ui_.customColors->setChecked(ui_.foreground->getColor().isValid() || ui_.background->getColor().isValid());
-		ui_.customSound->setChecked(!ui_.soundFile->text().isEmpty());
-		ui_.applyTo->focusWidget();
-	} else {
-		if (hasValidIndex_) {
-			mapper_->clearMapping();
-		}
-	}
-
-	hasValidIndex_ = index.isValid();
-}
-
-void QtHighlightRuleWidget::onCustomColorsToggled(bool enabled)
-{
-	if (!enabled) {
-		ui_.foreground->setColor(QColor());
-		ui_.background->setColor(QColor());
-	}
-	ui_.foreground->setEnabled(enabled);
-	ui_.background->setEnabled(enabled);
-}
-
-void QtHighlightRuleWidget::onCustomSoundToggled(bool enabled)
-{
-	if (enabled) {
-		if (ui_.soundFile->text().isEmpty()) {
-			onSoundFileButtonClicked();
-		}
-	} else {
-		ui_.soundFile->clear();
-	}
-	ui_.soundFile->setEnabled(enabled);
-	ui_.soundFileButton->setEnabled(enabled);
-}
-
-void QtHighlightRuleWidget::onSoundFileButtonClicked()
-{
-	QString s = QFileDialog::getOpenFileName(this, tr("Choose sound file"), QString(), tr("Sound files (*.wav)"));
-	if (!s.isEmpty()) {
-		ui_.soundFile->setText(s);
-	}
-}
-
-void QtHighlightRuleWidget::onHighlightTextToggled(bool enabled)
-{
-	ui_.customColors->setEnabled(enabled);
-}
-
-void QtHighlightRuleWidget::onPlaySoundToggled(bool enabled)
-{
-	ui_.customSound->setEnabled(enabled);
-}
-
-void QtHighlightRuleWidget::save()
-{
-	if (hasValidIndex_) {
-		mapper_->submit();
-	}
-}
-
-void QtHighlightRuleWidget::revert()
-{
-	if (hasValidIndex_) {
-		mapper_->revert();
-	}
-}
-
-}
diff --git a/Swift/QtUI/QtHighlightRuleWidget.h b/Swift/QtUI/QtHighlightRuleWidget.h
deleted file mode 100644
index 8a59a14..0000000
--- a/Swift/QtUI/QtHighlightRuleWidget.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2012 Maciej Niedzielski
- * Licensed under the simplified BSD license.
- * See Documentation/Licenses/BSD-simplified.txt for more information.
- */
-
-#pragma once
-
-#include <QWidget>
-#include <QModelIndex>
-
-#include <Swift/QtUI/ui_QtHighlightRuleWidget.h>
-
-class QDataWidgetMapper;
-
-namespace Swift {
-
-	class QtHighlightRulesItemModel;
-
-	class QtHighlightRuleWidget : public QWidget
-	{
-		Q_OBJECT
-
-		public:
-			explicit QtHighlightRuleWidget(QWidget* parent = NULL);
-			~QtHighlightRuleWidget();
-
-			void setModel(QtHighlightRulesItemModel* model);
-
-		public slots:
-			void setActiveIndex(const QModelIndex&);
-			void save();
-			void revert();
-
-		private slots:
-			void onHighlightTextToggled(bool);
-			void onCustomColorsToggled(bool);
-			void onPlaySoundToggled(bool);
-			void onCustomSoundToggled(bool);
-			void onSoundFileButtonClicked();
-
-		private:
-			QDataWidgetMapper * mapper_;
-			QtHighlightRulesItemModel * model_;
-			bool hasValidIndex_;
-			Ui::QtHighlightRuleWidget ui_;
-	};
-
-}
diff --git a/Swift/QtUI/QtHighlightRuleWidget.ui b/Swift/QtUI/QtHighlightRuleWidget.ui
deleted file mode 100644
index 9c465f9..0000000
--- a/Swift/QtUI/QtHighlightRuleWidget.ui
+++ /dev/null
@@ -1,260 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>QtHighlightRuleWidget</class>
- <widget class="QWidget" name="QtHighlightRuleWidget">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>361</width>
-    <height>524</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Form</string>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout_2">
-   <item>
-    <widget class="QGroupBox" name="groupBox">
-     <property name="title">
-      <string>Rule conditions</string>
-     </property>
-     <layout class="QFormLayout" name="formLayout">
-      <property name="fieldGrowthPolicy">
-       <enum>QFormLayout::ExpandingFieldsGrow</enum>
-      </property>
-      <item row="0" column="0" colspan="2">
-       <widget class="QLabel" name="label">
-        <property name="text">
-         <string>Choose when this rule should be applied.
-If you want to provide more than one sender or keyword, input them in separate lines.</string>
-        </property>
-        <property name="wordWrap">
-         <bool>true</bool>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0" colspan="2">
-       <widget class="Line" name="line">
-        <property name="orientation">
-         <enum>Qt::Horizontal</enum>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="0">
-       <widget class="QLabel" name="label_2">
-        <property name="text">
-         <string>&amp;Apply to:</string>
-        </property>
-        <property name="buddy">
-         <cstring>applyTo</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="1">
-       <widget class="QComboBox" name="applyTo"/>
-      </item>
-      <item row="3" column="0">
-       <widget class="QLabel" name="label_3">
-        <property name="text">
-         <string>&amp;Senders:</string>
-        </property>
-        <property name="buddy">
-         <cstring>senders</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="3" column="1">
-       <widget class="QPlainTextEdit" name="senders"/>
-      </item>
-      <item row="4" column="0">
-       <widget class="QLabel" name="label_4">
-        <property name="text">
-         <string>&amp;Keywords:</string>
-        </property>
-        <property name="buddy">
-         <cstring>keywords</cstring>
-        </property>
-       </widget>
-      </item>
-      <item row="4" column="1">
-       <widget class="QPlainTextEdit" name="keywords"/>
-      </item>
-      <item row="5" column="1">
-       <widget class="QCheckBox" name="nickIsKeyword">
-        <property name="text">
-         <string>Treat &amp;nick as a keyword (in MUC)</string>
-        </property>
-       </widget>
-      </item>
-      <item row="6" column="1">
-       <widget class="QCheckBox" name="matchWholeWords">
-        <property name="text">
-         <string>Match whole &amp;words</string>
-        </property>
-       </widget>
-      </item>
-      <item row="7" column="1">
-       <widget class="QCheckBox" name="matchCase">
-        <property name="text">
-         <string>Match &amp;case</string>
-        </property>
-       </widget>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item>
-    <widget class="QGroupBox" name="groupBox_2">
-     <property name="title">
-      <string>Actions</string>
-     </property>
-     <layout class="QVBoxLayout" name="verticalLayout">
-      <item>
-       <widget class="QCheckBox" name="highlightText">
-        <property name="text">
-         <string>&amp;Highlight text</string>
-        </property>
-       </widget>
-      </item>
-      <item>
-       <layout class="QHBoxLayout" name="horizontalLayout">
-        <item>
-         <spacer name="horizontalSpacer">
-          <property name="orientation">
-           <enum>Qt::Horizontal</enum>
-          </property>
-          <property name="sizeType">
-           <enum>QSizePolicy::Fixed</enum>
-          </property>
-          <property name="sizeHint" stdset="0">
-           <size>
-            <width>28</width>
-            <height>20</height>
-           </size>
-          </property>
-         </spacer>
-        </item>
-        <item>
-         <widget class="QCheckBox" name="customColors">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="text">
-           <string>Custom c&amp;olors:</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="Swift::QtColorToolButton" name="foreground">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="text">
-           <string>&amp;Foreground</string>
-          </property>
-          <property name="toolButtonStyle">
-           <enum>Qt::ToolButtonTextBesideIcon</enum>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="Swift::QtColorToolButton" name="background">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="text">
-           <string>&amp;Background</string>
-          </property>
-          <property name="toolButtonStyle">
-           <enum>Qt::ToolButtonTextBesideIcon</enum>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </item>
-      <item>
-       <widget class="QCheckBox" name="playSound">
-        <property name="text">
-         <string>&amp;Play sound</string>
-        </property>
-       </widget>
-      </item>
-      <item>
-       <layout class="QHBoxLayout" name="horizontalLayout_2">
-        <item>
-         <spacer name="horizontalSpacer_2">
-          <property name="orientation">
-           <enum>Qt::Horizontal</enum>
-          </property>
-          <property name="sizeType">
-           <enum>QSizePolicy::Fixed</enum>
-          </property>
-          <property name="sizeHint" stdset="0">
-           <size>
-            <width>28</width>
-            <height>20</height>
-           </size>
-          </property>
-         </spacer>
-        </item>
-        <item>
-         <widget class="QCheckBox" name="customSound">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="text">
-           <string>Custom soun&amp;d:</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QLineEdit" name="soundFile">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="readOnly">
-           <bool>true</bool>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QToolButton" name="soundFileButton">
-          <property name="enabled">
-           <bool>false</bool>
-          </property>
-          <property name="text">
-           <string>...</string>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </item>
-     </layout>
-    </widget>
-   </item>
-   <item>
-    <spacer name="verticalSpacer">
-     <property name="orientation">
-      <enum>Qt::Vertical</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>20</width>
-       <height>101</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>Swift::QtColorToolButton</class>
-   <extends>QToolButton</extends>
-   <header>QtColorToolButton.h</header>
-  </customwidget>
- </customwidgets>
- <resources/>
- <connections/>
-</ui>
diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp
index 701170c..b0c1492 100644
--- a/Swift/QtUI/QtUIFactory.cpp
+++ b/Swift/QtUI/QtUIFactory.cpp
@@ -25,7 +25,7 @@
 #include <Swift/QtUI/QtContactEditWindow.h>
 #include <Swift/QtUI/QtAdHocCommandWindow.h>
 #include <Swift/QtUI/QtFileTransferListWidget.h>
-#include <Swift/QtUI/QtHighlightEditorWidget.h>
+#include <Swift/QtUI/QtHighlightEditor.h>
 #include <Swift/QtUI/Whiteboard/QtWhiteboardWindow.h>
 #include <Swift/Controllers/Settings/SettingsProviderHierachy.h>
 #include <Swift/QtUI/QtUISettingConstants.h>
@@ -164,8 +164,8 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(boost::shared_ptr<Whiteboa
 	return new QtWhiteboardWindow(whiteboardSession);
 }
 
-HighlightEditorWidget* QtUIFactory::createHighlightEditorWidget() {
-	return new QtHighlightEditorWidget();
+HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() {
+	return new QtHighlightEditor(qtOnlySettings);
 }
 
 BlockListEditorWidget *QtUIFactory::createBlockListEditorWidget() {
diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h
index 4c50572..9c07e76 100644
--- a/Swift/QtUI/QtUIFactory.h
+++ b/Swift/QtUI/QtUIFactory.h
@@ -48,7 +48,7 @@ namespace Swift {
 			virtual ContactEditWindow* createContactEditWindow();
 			virtual FileTransferListWidget* createFileTransferListWidget();
 			virtual WhiteboardWindow* createWhiteboardWindow(boost::shared_ptr<WhiteboardSession> whiteboardSession);
-			virtual HighlightEditorWidget* createHighlightEditorWidget();
+			virtual HighlightEditorWindow* createHighlightEditorWindow();
 			virtual BlockListEditorWidget* createBlockListEditorWidget();
 			virtual AdHocCommandWindow* createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command);
 
diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp
index 23bc099..1486293 100644
--- a/Swift/QtUI/QtWebKitChatView.cpp
+++ b/Swift/QtUI/QtWebKitChatView.cpp
@@ -536,6 +536,22 @@ std::string QtWebKitChatView::addMessage(
 	return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, highlight, ChatSnippet::getDirection(message));
 }
 
+QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const std::string& background) {
+	QString ecsapeColor = QtUtilities::htmlEscape(P2QSTRING(text));
+	QString escapeBackground = QtUtilities::htmlEscape(P2QSTRING(background));
+	if (ecsapeColor.isEmpty()) {
+		ecsapeColor = "black";
+	}
+	if (escapeBackground.isEmpty()) {
+		escapeBackground = "yellow";
+	}
+	return QString("<span style=\"color: %1; background: %2\">").arg(ecsapeColor).arg(escapeBackground);
+}
+
+QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) {
+	return getHighlightSpanStart(highlight.getTextColor(), highlight.getTextBackground());
+}
+
 QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& message) {
 	QString result;
 	foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, message.getParts()) {
@@ -562,7 +578,8 @@ QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& messa
 			continue;
 		}
 		if ((highlightPart = boost::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part))) {
-			//FIXME: Maybe do something here. Anything, really.
+			QString spanStart = getHighlightSpanStart(highlightPart->foregroundColor, highlightPart->backgroundColor);
+			result += spanStart + QtUtilities::htmlEscape(P2QSTRING(highlightPart->text)) + "</span>";
 			continue;
 		}
 
@@ -570,20 +587,6 @@ QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& messa
 	return result;
 }
 
-
-QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) {
-	QString color = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextColor()));
-	QString background = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextBackground()));
-	if (color.isEmpty()) {
-		color = "black";
-	}
-	if (background.isEmpty()) {
-		background = "yellow";
-	}
-
-	return QString("<span style=\"color: %1; background: %2\">").arg(color).arg(background);
-}
-
 std::string QtWebKitChatView::addMessage(
 		const QString& message, 
 		const std::string& senderName, 
diff --git a/Swift/QtUI/QtWebKitChatView.h b/Swift/QtUI/QtWebKitChatView.h
index fb6e4da..925ceeb 100644
--- a/Swift/QtUI/QtWebKitChatView.h
+++ b/Swift/QtUI/QtWebKitChatView.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Remko Tronçon
+ * Copyright (c) 2010-2014 Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -148,8 +148,9 @@ namespace Swift {
 					const HighlightAction& highlight);
 			bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf);
 			static ChatSnippet::Direction getActualDirection(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction);
-			QString chatMessageToHTML(const ChatWindow::ChatMessage& message);
+			QString getHighlightSpanStart(const std::string& text, const std::string& background);
 			QString getHighlightSpanStart(const HighlightAction& highlight);
+			QString chatMessageToHTML(const ChatWindow::ChatMessage& message);
 			static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString());
 
 		private:
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index dd7d0c3..26e738a 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -127,9 +127,7 @@ sources = [
     "QtContactEditWindow.cpp",
     "QtContactEditWidget.cpp",
     "QtSingleWindow.cpp",
-    "QtHighlightEditorWidget.cpp",
-    "QtHighlightRulesItemModel.cpp",
-    "QtHighlightRuleWidget.cpp",
+    "QtHighlightEditor.cpp",
     "QtColorToolButton.cpp",
     "QtClosableLineEdit.cpp",
     "ChatSnippet.cpp",
@@ -286,8 +284,7 @@ myenv.Uic4("QtAffiliationEditor.ui")
 myenv.Uic4("QtJoinMUCWindow.ui")
 myenv.Uic4("QtHistoryWindow.ui")
 myenv.Uic4("QtConnectionSettings.ui")
-myenv.Uic4("QtHighlightRuleWidget.ui")
-myenv.Uic4("QtHighlightEditorWidget.ui")
+myenv.Uic4("QtHighlightEditor.ui")
 myenv.Uic4("QtBlockListEditorWindow.ui")
 myenv.Uic4("QtSpellCheckerWindow.ui")
 myenv.Qrc("DefaultTheme.qrc")
-- 
cgit v0.10.2-6-g49f6