From 4ed137080a3d80d20a2cead47f741e3dd2f2d42e Mon Sep 17 00:00:00 2001
From: Maciej Niedzielski <machekku@uaznia.net>
Date: Fri, 21 Dec 2012 20:58:24 +0100
Subject: Highlighting support

Change-Id: Ib6bd42cecff018998117bc1e7db279a62b3af434
License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details.

diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp
index 16b22fe..0ffef0c 100644
--- a/Swift/Controllers/Chat/ChatController.cpp
+++ b/Swift/Controllers/Chat/ChatController.cpp
@@ -32,7 +32,7 @@
 #include <Swiften/Elements/DeliveryReceipt.h>
 #include <Swiften/Elements/DeliveryReceiptRequest.h>
 #include <Swift/Controllers/SettingConstants.h>
-
+#include <Swift/Controllers/Highlighter.h>
 #include <Swiften/Base/Log.h>
 
 namespace Swift {
@@ -40,8 +40,8 @@ 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)
-	: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings) {
+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)
+	: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings) {
 	isInMUC_ = isInMUC;
 	lastWasPresence_ = false;
 	chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider);
@@ -174,8 +174,11 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> me
 	}
 }
 
-void ChatController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) {
+void ChatController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction& highlight) {
 	eventController_->handleIncomingEvent(messageEvent);
+	if (!messageEvent->getConcluded()) {
+		highlighter_->handleHighlightAction(highlight);
+	}
 }
 
 
@@ -211,9 +214,9 @@ 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());
+		replaceMessage(body, myLastMessageUIID_, 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>(), std::string(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time());
+		myLastMessageUIID_ = addMessage(body, QT_TRANSLATE_NOOP("", "me"), true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel().getLabel() : boost::shared_ptr<SecurityLabel>(), std::string(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time(), HighlightAction());
 	}
 
 	if (stanzaChannel_->getStreamManagementEnabled() && !myLastMessageUIID_.empty() ) {
diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h
index 66ec37d..6021ec1 100644
--- a/Swift/Controllers/Chat/ChatController.h
+++ b/Swift/Controllers/Chat/ChatController.h
@@ -22,10 +22,11 @@ namespace Swift {
 	class FileTransferController;
 	class SettingsProvider;
 	class HistoryController;
+	class HighlightManager;
 
 	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);
+			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);
 			virtual ~ChatController();
 			virtual void setToJID(const JID& jid);
 			virtual void setOnline(bool online);
@@ -45,7 +46,7 @@ namespace Swift {
 			bool isIncomingMessageFromMe(boost::shared_ptr<Message> message);
 			void postSendMessage(const std::string &body, boost::shared_ptr<Stanza> sentStanza);
 			void preHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent);
-			void postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent);
+			void postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction&);
 			void preSendMessageRequest(boost::shared_ptr<Message>);
 			std::string senderDisplayNameFromMessage(const JID& from);
 			virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message>) const;
diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp
index d380cd5..ad7f76a 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.cpp
+++ b/Swift/Controllers/Chat/ChatControllerBase.cpp
@@ -31,15 +31,18 @@
 #include <Swiften/Queries/Requests/GetSecurityLabelsCatalogRequest.h>
 #include <Swiften/Avatars/AvatarManager.h>
 #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h>
+#include <Swift/Controllers/HighlightManager.h>
+#include <Swift/Controllers/Highlighter.h>
 
 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) : 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) {
+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) : 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) {
 	chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream);
 	chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this));
 	chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2));
 	chatWindow_->onLogCleared.connect(boost::bind(&ChatControllerBase::handleLogCleared, this));
 	entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1));
+	highlighter_ = highlightManager->createHighlighter();
 	setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable());
 	createDayChangeTimer();
 }
@@ -176,19 +179,19 @@ void ChatControllerBase::activateChatWindow() {
 	chatWindow_->activate();
 }
 
-std::string ChatControllerBase::addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) {
+std::string ChatControllerBase::addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
 	if (boost::starts_with(message, "/me ")) {
-		return chatWindow_->addAction(String::getSplittedAtFirst(message, ' ').second, senderName, senderIsSelf, label, avatarPath, time);
+		return chatWindow_->addAction(String::getSplittedAtFirst(message, ' ').second, senderName, senderIsSelf, label, avatarPath, time, highlight);
 	} else {
-		return chatWindow_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time);
+		return chatWindow_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time, highlight);
 	}
 }
 
-void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) {
+void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
 	if (boost::starts_with(message, "/me ")) {
-		chatWindow_->replaceWithAction(String::getSplittedAtFirst(message, ' ').second, id, time);
+		chatWindow_->replaceWithAction(String::getSplittedAtFirst(message, ' ').second, id, time, highlight);
 	} else {
-		chatWindow_->replaceMessage(message, id, time);
+		chatWindow_->replaceMessage(message, id, time, highlight);
 	}
 }
 
@@ -206,6 +209,7 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m
 	}
 	boost::shared_ptr<Message> message = messageEvent->getStanza();
 	std::string body = message->getBody();
+	HighlightAction highlight;
 	if (message->isError()) {
 		std::string errorMessage = str(format(QT_TRANSLATE_NOOP("", "Couldn't send message: %1%")) % getErrorMessage(message->getPayload<ErrorPayload>()));
 		chatWindow_->addErrorMessage(errorMessage);
@@ -244,6 +248,11 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m
 		}
 		onActivity(body);
 
+		// Highlight
+		if (!isIncomingMessageFromMe(message)) {
+			 highlight = highlighter_->findAction(body, senderDisplayNameFromMessage(from));
+		}
+
  		boost::shared_ptr<Replace> replace = message->getPayload<Replace>();
 		if (replace) {
 			std::string body = message->getBody();
@@ -251,11 +260,11 @@ 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);
+				replaceMessage(body, lastMessagesUIID_[from], timeStamp, highlight);
 			}
 		}
 		else {
-			lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, std::string(avatarManager_->getAvatarPath(from).string()), timeStamp);
+			lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, std::string(avatarManager_->getAvatarPath(from).string()), timeStamp, highlight);
 		}
 
 		logMessage(body, from, selfJID_, timeStamp, true);
@@ -263,7 +272,7 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m
 	chatWindow_->show();
 	chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size()));
 	onUnreadCountChanged();
-	postHandleIncomingMessage(messageEvent);
+	postHandleIncomingMessage(messageEvent, highlight);
 }
 
 std::string ChatControllerBase::getErrorMessage(boost::shared_ptr<ErrorPayload> error) {
diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h
index 02cf9f6..baef9e6 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.h
+++ b/Swift/Controllers/Chat/ChatControllerBase.h
@@ -29,6 +29,7 @@
 #include "Swiften/Base/IDGenerator.h"
 #include <Swift/Controllers/HistoryController.h>
 #include <Swiften/MUC/MUCRegistry.h>
+#include <Swift/Controllers/HighlightManager.h>
 
 namespace Swift {
 	class IQRouter;
@@ -39,6 +40,8 @@ namespace Swift {
 	class UIEventStream;
 	class EventController;
 	class EntityCapsProvider;
+	class HighlightManager;
+	class Highlighter;
 
 	class ChatControllerBase : public boost::bsignals::trackable {
 		public:
@@ -47,8 +50,8 @@ namespace Swift {
 			void activateChatWindow();
 			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 std::string& avatarPath, const boost::posix_time::ptime& time);
-			void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time);
+			std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& 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);
 			virtual void setOnline(bool online);
 			virtual void setEnabled(bool enabled);
 			virtual void setToJID(const JID& jid) {toJID_ = jid;}
@@ -60,7 +63,7 @@ namespace Swift {
 			void handleCapsChanged(const JID& jid);
 
 		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);
+			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);
 
 			/**
 			 * Pass the Message appended, and the stanza used to send it.
@@ -69,7 +72,7 @@ namespace Swift {
 			virtual std::string senderDisplayNameFromMessage(const JID& from) = 0;
 			virtual bool isIncomingMessageFromMe(boost::shared_ptr<Message>) = 0;
 			virtual void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {}
-			virtual void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {}
+			virtual void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>, const HighlightAction&) {}
 			virtual void preSendMessageRequest(boost::shared_ptr<Message>) {}
 			virtual bool isFromContact(const JID& from);
 			virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message>) const = 0;
@@ -116,5 +119,6 @@ namespace Swift {
 			SecurityLabelsCatalog::Item lastLabel_; 
 			HistoryController* historyController_;
 			MUCRegistry* mucRegistry_;
+			Highlighter* highlighter_;
 	};
 }
diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp
index 1e0e9c2..dba8565 100644
--- a/Swift/Controllers/Chat/ChatsManager.cpp
+++ b/Swift/Controllers/Chat/ChatsManager.cpp
@@ -74,7 +74,8 @@ ChatsManager::ChatsManager(
 		bool eagleMode,
 		SettingsProvider* settings,
 		HistoryController* historyController,
-		WhiteboardManager* whiteboardManager) :
+		WhiteboardManager* whiteboardManager,
+		HighlightManager* highlightManager) :
 			jid_(jid), 
 			joinMUCWindowFactory_(joinMUCWindowFactory), 
 			useDelayForLatency_(useDelayForLatency), 
@@ -86,7 +87,8 @@ ChatsManager::ChatsManager(
 			eagleMode_(eagleMode),
 			settings_(settings),
 			historyController_(historyController),
-			whiteboardManager_(whiteboardManager) {
+			whiteboardManager_(whiteboardManager),
+			highlightManager_(highlightManager) {
 	timerFactory_ = timerFactory;
 	eventController_ = eventController;
 	stanzaChannel_ = stanzaChannel;
@@ -521,7 +523,7 @@ 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_);
+	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_);
 	chatControllers_[contact] = controller;
 	controller->setAvailableServerFeatures(serverDiscoInfo_);
 	controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false));
@@ -594,7 +596,7 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional
 		if (createAsReservedIfNew) {
 			muc->setCreateAsReservedIfNew();
 		}
-		MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_);
+		MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_);
 		mucControllers_[mucJID] = controller;
 		controller->setAvailableServerFeatures(serverDiscoInfo_);
 		controller->onUserLeft.connect(boost::bind(&ChatsManager::handleUserLeftMUC, this, controller));
diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h
index 5b8b785..55e62b9 100644
--- a/Swift/Controllers/Chat/ChatsManager.h
+++ b/Swift/Controllers/Chat/ChatsManager.h
@@ -50,10 +50,11 @@ namespace Swift {
 	class SettingsProvider;
 	class WhiteboardManager;
 	class HistoryController;
+	class HighlightManager;
 	
 	class ChatsManager {
 		public:
-			ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager);
+			ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager, HighlightManager* highlightManager);
 			virtual ~ChatsManager();
 			void setAvatarManager(AvatarManager* avatarManager);
 			void setOnline(bool enabled);
@@ -136,5 +137,6 @@ namespace Swift {
 			SettingsProvider* settings_;
 			HistoryController* historyController_;
 			WhiteboardManager* whiteboardManager_;
+			HighlightManager* highlightManager_;
 	};
 }
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index d966d3f..937116f 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -35,6 +35,7 @@
 #include <Swift/Controllers/Roster/SetPresence.h>
 #include <Swiften/Disco/EntityCapsProvider.h>
 #include <Swiften/Roster/XMPPRoster.h>
+#include <Swift/Controllers/Highlighter.h>
 
 
 #define MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS 60000
@@ -61,8 +62,9 @@ MUCController::MUCController (
 		EntityCapsProvider* entityCapsProvider,
 		XMPPRoster* roster,
 		HistoryController* historyController,
-		MUCRegistry* mucRegistry) :
-			ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry), muc_(muc), nick_(nick), desiredNick_(nick), password_(password) {
+		MUCRegistry* mucRegistry,
+		HighlightManager* highlightManager) :
+			ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager), muc_(muc), nick_(nick), desiredNick_(nick), password_(password) {
 	parting_ = true;
 	joined_ = false;
 	lastWasPresence_ = false;
@@ -98,6 +100,8 @@ MUCController::MUCController (
 	muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1));
 	muc_->onRoleChangeFailed.connect(boost::bind(&MUCController::handleOccupantRoleChangeFailed, this, _1, _2, _3));
 	muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2));
+	highlighter_->setMode(Highlighter::MUCMode);
+	highlighter_->setNick(nick_);
 	if (timerFactory) {
 		loginCheckTimer_ = boost::shared_ptr<Timer>(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS));
 		loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this));
@@ -273,7 +277,7 @@ void MUCController::handleJoinFailed(boost::shared_ptr<ErrorPayload> error) {
 	chatWindow_->addErrorMessage(errorMessage);
 	parting_ = true;
 	if (!rejoinNick.empty()) {
-		nick_ = rejoinNick;
+		setNick(rejoinNick);
 		rejoin();
 	}
 }
@@ -284,7 +288,7 @@ void MUCController::handleJoinComplete(const std::string& nick) {
 	receivedActivity();
 	joined_ = true;
 	std::string joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick);
-	nick_ = nick;
+	setNick(nick);
 	chatWindow_->addSystemMessage(joinMessage);
 
 #ifdef SWIFT_EXPERIMENTAL_HISTORY
@@ -455,10 +459,13 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes
 	}
 }
 
-void MUCController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) {
+void MUCController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction& highlight) {
 	boost::shared_ptr<Message> message = messageEvent->getStanza();
 	if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && messageTargetsMe(message) && !message->getPayload<Delay>()) {
 		eventController_->handleIncomingEvent(messageEvent);
+		if (!messageEvent->getConcluded()) {
+			highlighter_->handleHighlightAction(highlight);
+		}
 	}
 }
 
@@ -510,7 +517,7 @@ void MUCController::setOnline(bool online) {
 			if (loginCheckTimer_) {
 				loginCheckTimer_->start();
 			}
-			nick_ = desiredNick_;
+			setNick(desiredNick_);
 			rejoin();
 		}
 	}
@@ -818,7 +825,7 @@ void MUCController::addRecentLogs() {
 		bool senderIsSelf = nick_ == message.getFromJID().getResource();
 
 		// the chatWindow uses utc timestamps
-		addMessage(message.getMessage(), senderDisplayNameFromMessage(message.getFromJID()), senderIsSelf, boost::shared_ptr<SecurityLabel>(new SecurityLabel()), std::string(avatarManager_->getAvatarPath(message.getFromJID()).string()), message.getTime() - boost::posix_time::hours(message.getOffset()));
+		addMessage(message.getMessage(), senderDisplayNameFromMessage(message.getFromJID()), senderIsSelf, boost::shared_ptr<SecurityLabel>(new SecurityLabel()), std::string(avatarManager_->getAvatarPath(message.getFromJID()).string()), message.getTime() - boost::posix_time::hours(message.getOffset()), HighlightAction());
 	}
 }
 
@@ -847,4 +854,10 @@ void MUCController::checkDuplicates(boost::shared_ptr<Message> newMessage) {
 	}
 }
 
+void MUCController::setNick(const std::string& nick)
+{
+	nick_ = nick;
+	highlighter_->setNick(nick_);
+}
+
 }
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index 7e81f3d..11fe0ff 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -33,6 +33,7 @@ namespace Swift {
 	class TabComplete;
 	class InviteToChatWindow;
 	class XMPPRoster;
+	class HighlightManager;
 
 	enum JoinPart {Join, Part, JoinThenPart, PartThenJoin};
 
@@ -44,7 +45,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);
+			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);
 			~MUCController();
 			boost::signal<void ()> onUserLeft;
 			boost::signal<void ()> onUserJoined;
@@ -62,7 +63,7 @@ namespace Swift {
 			std::string senderDisplayNameFromMessage(const JID& from);
 			boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message> message) const;
 			void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>);
-			void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>);
+			void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>, const HighlightAction&);
 			void cancelReplaces();
 			void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming);
 
@@ -108,6 +109,7 @@ namespace Swift {
 			void handleInviteToMUCWindowCompleted();
 			void addRecentLogs();
 			void checkDuplicates(boost::shared_ptr<Message> newMessage);
+			void setNick(const std::string& nick);
 
 		private:
 			MUC::ref muc_;
diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp
index aab582c..dd90d66 100644
--- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp
+++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp
@@ -106,14 +106,16 @@ public:
 		avatarManager_ = new NullAvatarManager();
 		wbSessionManager_ = new WhiteboardSessionManager(iqRouter_, stanzaChannel_, presenceOracle_, entityCapsManager_);
 		wbManager_ = new WhiteboardManager(whiteboardWindowFactory_, uiEventStream_, nickResolver_, wbSessionManager_);
+		highlightManager_ = new HighlightManager(settings_);
 
 		mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_);
-		manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, NULL, wbManager_);
+		manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, NULL, wbManager_, highlightManager_);
 
 		manager_->setAvatarManager(avatarManager_);
 	}
 	
 	void tearDown() {
+		delete highlightManager_;
 		//delete chatListWindowFactory
 		delete profileSettings_;
 		delete avatarManager_;
@@ -481,6 +483,7 @@ private:
 	FileTransferManager* ftManager_;
 	WhiteboardSessionManager* wbSessionManager_;
 	WhiteboardManager* wbManager_;
+	HighlightManager* highlightManager_;
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(ChatsManagerTest);
diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
index ab83bc2..f1fcf79 100644
--- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
+++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
@@ -26,6 +26,7 @@
 #include "Swiften/Network/TimerFactory.h"
 #include "Swiften/Elements/MUCUserPayload.h"
 #include "Swiften/Disco/DummyEntityCapsProvider.h"
+#include <Swift/Controllers/Settings/DummySettingsProvider.h>
 
 using namespace Swift;
 
@@ -62,12 +63,16 @@ public:
 		window_ = new MockChatWindow();
 		mucRegistry_ = new MUCRegistry();
 		entityCapsProvider_ = new DummyEntityCapsProvider();
+		settings_ = new DummySettingsProvider();
+		highlightManager_ = new HighlightManager(settings_);
 		muc_ = boost::make_shared<MUC>(stanzaChannel_, iqRouter_, directedPresenceSender_, mucJID_, mucRegistry_);
 		mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_);
-		controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_);
+		controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_, highlightManager_);
 	}
 
 	void tearDown() {
+		delete highlightManager_;
+		delete settings_;
 		delete entityCapsProvider_;
 		delete controller_;
 		delete eventController_;
@@ -338,6 +343,8 @@ private:
 	MockChatWindow* window_;
 	MUCRegistry* mucRegistry_;
 	DummyEntityCapsProvider* entityCapsProvider_;
+	DummySettingsProvider* settings_;
+	HighlightManager* highlightManager_;
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(MUCControllerTest);
diff --git a/Swift/Controllers/HighlightAction.cpp b/Swift/Controllers/HighlightAction.cpp
new file mode 100644
index 0000000..d4d199d
--- /dev/null
+++ b/Swift/Controllers/HighlightAction.cpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <Swift/Controllers/HighlightAction.h>
+
+namespace Swift {
+
+void HighlightAction::setHighlightText(bool highlightText)
+{
+	highlightText_ = highlightText;
+	if (!highlightText_) {
+		textColor_.clear();
+		textBackground_.clear();
+	}
+}
+
+void HighlightAction::setPlaySound(bool playSound)
+{
+	playSound_ = playSound;
+	if (!playSound_) {
+		soundFile_.clear();
+	}
+}
+
+}
diff --git a/Swift/Controllers/HighlightAction.h b/Swift/Controllers/HighlightAction.h
new file mode 100644
index 0000000..bfbed74
--- /dev/null
+++ b/Swift/Controllers/HighlightAction.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace Swift {
+
+	class HighlightRule;
+
+	class HighlightAction {
+		public:
+			HighlightAction() : highlightText_(false), playSound_(false) {}
+
+			bool highlightText() const { return highlightText_; }
+			void setHighlightText(bool highlightText);
+
+			const std::string& getTextColor() const { return textColor_; }
+			void setTextColor(const std::string& textColor) { textColor_ = textColor; }
+
+			const std::string& getTextBackground() const { return textBackground_; }
+			void setTextBackground(const std::string& textBackground) { textBackground_ = textBackground; }
+
+			bool playSound() const { return playSound_; }
+			void setPlaySound(bool playSound);
+
+			const std::string& getSoundFile() const { return soundFile_; }
+			void setSoundFile(const std::string& soundFile) { soundFile_ = soundFile; }
+
+			bool isEmpty() const { return !highlightText_ && !playSound_; }
+
+		private:
+			bool highlightText_;
+			std::string textColor_;
+			std::string textBackground_;
+
+			bool playSound_;
+			std::string soundFile_;
+	};
+
+}
diff --git a/Swift/Controllers/HighlightEditorController.cpp b/Swift/Controllers/HighlightEditorController.cpp
new file mode 100644
index 0000000..899e4bb
--- /dev/null
+++ b/Swift/Controllers/HighlightEditorController.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.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>
+
+namespace Swift {
+
+HighlightEditorController::HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWidgetFactory* highlightEditorWidgetFactory, HighlightManager* highlightManager) : highlightEditorWidgetFactory_(highlightEditorWidgetFactory), highlightEditorWidget_(NULL), highlightManager_(highlightManager)
+{
+	uiEventStream->onUIEvent.connect(boost::bind(&HighlightEditorController::handleUIEvent, this, _1));
+}
+
+HighlightEditorController::~HighlightEditorController()
+{
+	delete highlightEditorWidget_;
+	highlightEditorWidget_ = 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_);
+		}
+		highlightEditorWidget_->show();
+	}
+}
+
+}
diff --git a/Swift/Controllers/HighlightEditorController.h b/Swift/Controllers/HighlightEditorController.h
new file mode 100644
index 0000000..3868251
--- /dev/null
+++ b/Swift/Controllers/HighlightEditorController.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+
+namespace Swift {
+
+	class UIEventStream;
+
+	class HighlightEditorWidgetFactory;
+	class HighlightEditorWidget;
+
+	class HighlightManager;
+
+	class HighlightEditorController {
+		public:
+			HighlightEditorController(UIEventStream* uiEventStream, HighlightEditorWidgetFactory* highlightEditorWidgetFactory, HighlightManager* highlightManager);
+			~HighlightEditorController();
+
+			HighlightManager* getHighlightManager() const { return highlightManager_; }
+
+		private:
+			void handleUIEvent(boost::shared_ptr<UIEvent> event);
+
+		private:
+			HighlightEditorWidgetFactory* highlightEditorWidgetFactory_;
+			HighlightEditorWidget* highlightEditorWidget_;
+			HighlightManager* highlightManager_;
+	};
+
+}
diff --git a/Swift/Controllers/HighlightManager.cpp b/Swift/Controllers/HighlightManager.cpp
new file mode 100644
index 0000000..74a07c0
--- /dev/null
+++ b/Swift/Controllers/HighlightManager.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.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 <Swiften/Base/foreach.h>
+#include <Swift/Controllers/HighlightManager.h>
+#include <Swift/Controllers/Highlighter.h>
+#include <Swift/Controllers/Settings/SettingsProvider.h>
+#include <Swift/Controllers/SettingConstants.h>
+
+/* How does highlighting work?
+ *
+ * HighlightManager manages a list of if-then rules used to highlight messages.
+ * Rule is represented by HighlightRule. Action ("then" part) is HighlightAction.
+ *
+ *
+ * HighlightManager is also used as a factory for Highlighter objects.
+ * Each ChatControllerBase has its own Highlighter.
+ * Highligher may be customized by using setNick(), etc.
+ *
+ * ChatControllerBase passes incoming messages to Highlighter and gets HighlightAction in return
+ * (first matching rule is returned).
+ * If needed, HighlightAction is then passed back to Highlighter for further handling.
+ * This results in HighlightManager emiting onHighlight event,
+ * which is handled by SoundController to play sound notification
+ */
+
+namespace Swift {
+
+HighlightManager::HighlightManager(SettingsProvider* settings)
+	: settings_(settings)
+	, storingSettings_(false)
+{
+	loadSettings();
+	settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1));
+}
+
+void HighlightManager::handleSettingChanged(const std::string& settingPath)
+{
+	if (!storingSettings_ && SettingConstants::HIGHLIGHT_RULES.getKey() == settingPath) {
+		loadSettings();
+	}
+}
+
+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::vector<HighlightRule> HighlightManager::getDefaultRules()
+{
+	std::vector<HighlightRule> rules;
+	HighlightRule r;
+	r.setMatchChat(true);
+	r.getAction().setPlaySound(true);
+	rules.push_back(r);
+	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 && boost::numeric_cast<std::vector<std::string>::size_type>(index) < rules_.size());
+	return rules_[index];
+}
+
+void HighlightManager::setRule(int index, const HighlightRule& rule)
+{
+	assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) < rules_.size());
+	rules_[index] = rule;
+	storeSettings();
+}
+
+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();
+}
+
+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();
+}
+
+Highlighter* HighlightManager::createHighlighter()
+{
+	return new Highlighter(this);
+}
+
+}
diff --git a/Swift/Controllers/HighlightManager.h b/Swift/Controllers/HighlightManager.h
new file mode 100644
index 0000000..d195d05
--- /dev/null
+++ b/Swift/Controllers/HighlightManager.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <vector>
+#include <string>
+
+#include <Swiften/Base/boost_bsignals.h>
+#include <Swift/Controllers/HighlightRule.h>
+
+namespace Swift {
+
+	class SettingsProvider;
+	class Highlighter;
+
+	class HighlightManager {
+		public:
+			HighlightManager(SettingsProvider* settings);
+
+			Highlighter* createHighlighter();
+
+			const std::vector<HighlightRule>& 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);
+
+			boost::signal<void (const HighlightAction&)> onHighlight;
+
+		private:
+			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_;
+	};
+
+}
diff --git a/Swift/Controllers/HighlightRule.cpp b/Swift/Controllers/HighlightRule.cpp
new file mode 100644
index 0000000..01d1228
--- /dev/null
+++ b/Swift/Controllers/HighlightRule.cpp
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <algorithm>
+#include <boost/algorithm/string.hpp>
+#include <boost/lambda/lambda.hpp>
+
+#include <Swiften/Base/foreach.h>
+#include <Swift/Controllers/HighlightRule.h>
+
+namespace Swift {
+
+HighlightRule::HighlightRule()
+	: nickIsKeyword_(false)
+	, matchCase_(false)
+	, matchWholeWords_(false)
+	, matchChat_(false)
+	, matchMUC_(false)
+{
+}
+
+boost::regex HighlightRule::regexFromString(const std::string & s) const
+{
+	// escape regex special characters: ^.$| etc
+	// these need to be escaped: [\^\$\|.........]
+	// and then C++ requires '\' to be escaped, too....
+	static const boost::regex esc("([\\^\\.\\$\\|\\(\\)\\[\\]\\*\\+\\?\\/\\{\\}\\\\])");
+	// matched character should be prepended with '\'
+	// replace matched special character with \\\1
+	// and escape once more for C++ rules...
+	static const std::string rep("\\\\\\1");
+	std::string escaped = boost::regex_replace(s , esc, rep);
+
+	std::string word = matchWholeWords_ ? "\\b" : "";
+	boost::regex::flag_type flags = boost::regex::normal;
+	if (!matchCase_) {
+		flags |= boost::regex::icase;
+	}
+	return boost::regex(word + escaped + word, flags);
+}
+
+void HighlightRule::updateRegex() const
+{
+	keywordRegex_.clear();
+	foreach (const std::string & k, keywords_) {
+		keywordRegex_.push_back(regexFromString(k));
+	}
+	senderRegex_.clear();
+	foreach (const std::string & s, senders_) {
+		senderRegex_.push_back(regexFromString(s));
+	}
+}
+
+std::string HighlightRule::boolToString(bool b)
+{
+	return b ? "1" : "0";
+}
+
+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;
+	int 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_)) {
+
+		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;
+			}
+		}
+
+		foreach (const boost::regex & rx, senderRegex_) {
+			if (boost::regex_search(sender, rx)) {
+				matchesSender = true;
+				break;
+			}
+		}
+
+		if (matchesKeyword && matchesSender) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void HighlightRule::setSenders(const std::vector<std::string>& senders)
+{
+	senders_ = senders;
+	updateRegex();
+}
+
+void HighlightRule::setKeywords(const std::vector<std::string>& keywords)
+{
+	keywords_ = keywords;
+	updateRegex();
+}
+
+void HighlightRule::setNickIsKeyword(bool nickIsKeyword)
+{
+	nickIsKeyword_ = nickIsKeyword;
+	updateRegex();
+}
+
+void HighlightRule::setMatchCase(bool matchCase)
+{
+	matchCase_ = matchCase;
+	updateRegex();
+}
+
+void HighlightRule::setMatchWholeWords(bool matchWholeWords)
+{
+	matchWholeWords_ = matchWholeWords;
+	updateRegex();
+}
+
+void HighlightRule::setMatchChat(bool matchChat)
+{
+	matchChat_ = matchChat;
+	updateRegex();
+}
+
+void HighlightRule::setMatchMUC(bool matchMUC)
+{
+	matchMUC_ = matchMUC;
+	updateRegex();
+}
+
+bool HighlightRule::isEmpty() const
+{
+	return senders_.empty() && keywords_.empty() && !nickIsKeyword_ && !matchChat_ && !matchMUC_ && action_.isEmpty();
+}
+
+}
diff --git a/Swift/Controllers/HighlightRule.h b/Swift/Controllers/HighlightRule.h
new file mode 100644
index 0000000..1abfa5a
--- /dev/null
+++ b/Swift/Controllers/HighlightRule.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <vector>
+#include <string>
+
+#include <boost/regex.hpp>
+
+#include <Swift/Controllers/HighlightAction.h>
+
+namespace Swift {
+
+	class HighlightRule {
+		public:
+			HighlightRule();
+
+			enum MessageType { ChatMessage, MUCMessage };
+
+			bool isMatch(const std::string& body, const std::string& sender, const std::string& nick, MessageType) const;
+
+			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<std::string>& getKeywords() const { return keywords_; }
+			void setKeywords(const std::vector<std::string>&);
+
+			bool getNickIsKeyword() const { return nickIsKeyword_; }
+			void setNickIsKeyword(bool);
+
+			bool getMatchCase() const { return matchCase_; }
+			void setMatchCase(bool);
+
+			bool getMatchWholeWords() const { return matchWholeWords_; }
+			void setMatchWholeWords(bool);
+
+			bool getMatchChat() const { return matchChat_; }
+			void setMatchChat(bool);
+
+			bool getMatchMUC() const { return matchMUC_; }
+			void setMatchMUC(bool);
+
+			bool isEmpty() const;
+
+		private:
+			static std::string boolToString(bool);
+			static bool boolFromString(const std::string&);
+
+			std::vector<std::string> senders_;
+			std::vector<std::string> keywords_;
+			bool nickIsKeyword_;
+
+			mutable std::vector<boost::regex> senderRegex_;
+			mutable std::vector<boost::regex> keywordRegex_;
+			void updateRegex() const;
+			boost::regex regexFromString(const std::string&) const;
+
+			bool matchCase_;
+			bool matchWholeWords_;
+
+			bool matchChat_;
+			bool matchMUC_;
+
+			HighlightAction action_;
+	};
+
+}
diff --git a/Swift/Controllers/Highlighter.cpp b/Swift/Controllers/Highlighter.cpp
new file mode 100644
index 0000000..754641a
--- /dev/null
+++ b/Swift/Controllers/Highlighter.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <Swiften/Base/foreach.h>
+#include <Swift/Controllers/Highlighter.h>
+#include <Swift/Controllers/HighlightManager.h>
+
+namespace Swift {
+
+Highlighter::Highlighter(HighlightManager* manager)
+	: manager_(manager)
+{
+	setMode(ChatMode);
+}
+
+void Highlighter::setMode(Mode mode)
+{
+	mode_ = mode;
+	messageType_ = mode_ == ChatMode ? HighlightRule::ChatMessage : HighlightRule::MUCMessage;
+}
+
+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();
+		}
+	}
+
+	return HighlightAction();
+}
+
+void Highlighter::handleHighlightAction(const HighlightAction& action)
+{
+	manager_->onHighlight(action);
+}
+
+}
diff --git a/Swift/Controllers/Highlighter.h b/Swift/Controllers/Highlighter.h
new file mode 100644
index 0000000..d026f29
--- /dev/null
+++ b/Swift/Controllers/Highlighter.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <string>
+
+#include <Swift/Controllers/HighlightRule.h>
+
+namespace Swift {
+
+	class HighlightManager;
+
+	class Highlighter {
+		public:
+			Highlighter(HighlightManager* manager);
+
+			enum Mode { ChatMode, MUCMode };
+			void setMode(Mode mode);
+
+			void setNick(const std::string& nick) { nick_ = nick; }
+
+			HighlightAction findAction(const std::string& body, const std::string& sender) const;
+
+			void handleHighlightAction(const HighlightAction& action);
+
+		private:
+			HighlightManager* manager_;
+			Mode mode_;
+			HighlightRule::MessageType messageType_;
+			std::string nick_;
+	};
+
+}
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 195eeaf..bb74ed7 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -84,6 +84,8 @@
 #include <Swiften/Client/ClientXMLTracer.h>
 #include <Swift/Controllers/SettingConstants.h>
 #include <Swiften/Client/StanzaChannel.h>
+#include <Swift/Controllers/HighlightManager.h>
+#include <Swift/Controllers/HighlightEditorController.h>
 
 namespace Swift {
 
@@ -148,7 +150,11 @@ MainController::MainController(
 	systemTrayController_ = new SystemTrayController(eventController_, systemTray);
 	loginWindow_ = uiFactory_->createLoginWindow(uiEventStream_);
 	loginWindow_->setShowNotificationToggle(!notifier->isExternallyConfigured());
-	soundEventController_ = new SoundEventController(eventController_, soundPlayer, settings);
+
+	highlightManager_ = new HighlightManager(settings_);
+	highlightEditorController_ = new HighlightEditorController(uiEventStream_, uiFactory_, highlightManager_);
+
+	soundEventController_ = new SoundEventController(eventController_, soundPlayer, settings, highlightManager_);
 
 	xmppURIController_ = new XMPPURIController(uriHandler_, uiEventStream_);
 
@@ -208,6 +214,8 @@ MainController::~MainController() {
 	eventController_->disconnectAll();
 
 	resetClient();
+	delete highlightEditorController_;
+	delete highlightManager_;
 	delete fileTransferListController_;
 	delete xmlConsoleController_;
 	delete xmppURIController_;
@@ -324,9 +332,9 @@ void MainController::handleConnected() {
 #ifdef SWIFT_EXPERIMENTAL_HISTORY
 		historyController_ = new HistoryController(storages_->getHistoryStorage());
 		historyViewController_ = new HistoryViewController(jid_, uiEventStream_, historyController_, client_->getNickResolver(), client_->getAvatarManager(), client_->getPresenceOracle(), uiFactory_);
-		chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, historyController_, whiteboardManager_);
+		chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, historyController_, whiteboardManager_, highlightManager_);
 #else
-		chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, NULL, whiteboardManager_);
+		chatsManager_ = new ChatsManager(jid_, client_->getStanzaChannel(), client_->getIQRouter(), eventController_, uiFactory_, uiFactory_, client_->getNickResolver(), client_->getPresenceOracle(), client_->getPresenceSender(), uiEventStream_, uiFactory_, useDelayForLatency_, networkFactories_->getTimerFactory(), client_->getMUCRegistry(), client_->getEntityCapsProvider(), client_->getMUCManager(), uiFactory_, profileSettings_, ftOverview_, client_->getRoster(), !settings_->getSetting(SettingConstants::REMEMBER_RECENT_CHATS), settings_, NULL, whiteboardManager_, highlightManager_);
 #endif
 		
 		client_->onMessageReceived.connect(boost::bind(&ChatsManager::handleIncomingMessage, chatsManager_, _1));
diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h
index 2e5bd05..fc8d518 100644
--- a/Swift/Controllers/MainController.h
+++ b/Swift/Controllers/MainController.h
@@ -71,6 +71,8 @@ namespace Swift {
 	class AdHocCommandWindowFactory;
 	class FileTransferOverview;
 	class WhiteboardManager;
+	class HighlightManager;
+	class HighlightEditorController;
 
 	class MainController {
 		public:
@@ -176,5 +178,7 @@ namespace Swift {
 			static const int SecondsToWaitBeforeForceQuitting;
 			FileTransferOverview* ftOverview_;
 			WhiteboardManager* whiteboardManager_;
+			HighlightManager* highlightManager_;
+			HighlightEditorController* highlightEditorController_;
 	};
 }
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index a54c6a2..cd88dd9 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -75,7 +75,12 @@ if env["SCONS_STAGE"] == "build" :
 			"ChatMessageSummarizer.cpp",
 			"SettingConstants.cpp",
 			"WhiteboardManager.cpp",
-			"StatusCache.cpp"
+			"StatusCache.cpp",
+			"HighlightAction.cpp",
+			"HighlightEditorController.cpp",
+			"HighlightManager.cpp",
+			"HighlightRule.cpp",
+			"Highlighter.cpp"
 		])
 
 	env.Append(UNITTEST_SOURCES = [
@@ -90,4 +95,5 @@ if env["SCONS_STAGE"] == "build" :
 			File("UnitTest/MockChatWindow.cpp"),
 			File("UnitTest/ChatMessageSummarizerTest.cpp"),
 			File("Settings/UnitTest/SettingsProviderHierachyTest.cpp"),
+			File("UnitTest/HighlightRuleTest.cpp"),
 		])
diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp
index 7ab4ac4..e430c77 100644
--- a/Swift/Controllers/SettingConstants.cpp
+++ b/Swift/Controllers/SettingConstants.cpp
@@ -19,4 +19,5 @@ const SettingsProvider::Setting<bool> SettingConstants::LOGIN_AUTOMATICALLY = Se
 const SettingsProvider::Setting<bool> SettingConstants::SHOW_OFFLINE("showOffline", false);
 const SettingsProvider::Setting<std::string> SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", "");
 const SettingsProvider::Setting<bool> SettingConstants::PLAY_SOUNDS("playSounds", true);
+const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES("highlightRules", "@");
 }
diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h
index ff1ed72..cc3af47 100644
--- a/Swift/Controllers/SettingConstants.h
+++ b/Swift/Controllers/SettingConstants.h
@@ -22,5 +22,6 @@ namespace Swift {
 			static const SettingsProvider::Setting<bool> SHOW_OFFLINE;
 			static const SettingsProvider::Setting<std::string> EXPANDED_ROSTER_GROUPS;
 			static const SettingsProvider::Setting<bool> PLAY_SOUNDS;
+			static const SettingsProvider::Setting<std::string> HIGHLIGHT_RULES;
 	};
 }
diff --git a/Swift/Controllers/SoundEventController.cpp b/Swift/Controllers/SoundEventController.cpp
index d056990..a5171e2 100644
--- a/Swift/Controllers/SoundEventController.cpp
+++ b/Swift/Controllers/SoundEventController.cpp
@@ -12,22 +12,33 @@
 #include <Swift/Controllers/SoundPlayer.h>
 #include <Swift/Controllers/UIEvents/UIEventStream.h>
 #include <Swift/Controllers/SettingConstants.h>
+#include <Swift/Controllers/HighlightManager.h>
 
 namespace Swift {
 
-SoundEventController::SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings) {
+SoundEventController::SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings, HighlightManager* highlightManager) {
 	settings_ = settings;
 	eventController_ = eventController;
 	soundPlayer_ = soundPlayer;
 	eventController_->onEventQueueEventAdded.connect(boost::bind(&SoundEventController::handleEventQueueEventAdded, this, _1));
+	highlightManager_ = highlightManager;
+	highlightManager_->onHighlight.connect(boost::bind(&SoundEventController::handleHighlight, this, _1));
+
 	settings_->onSettingChanged.connect(boost::bind(&SoundEventController::handleSettingChanged, this, _1));
 
 	playSounds_ = settings->getSetting(SettingConstants::PLAY_SOUNDS);
 }
 
-void SoundEventController::handleEventQueueEventAdded(boost::shared_ptr<StanzaEvent> event) {
-	if (playSounds_ && !event->getConcluded()) {
-		soundPlayer_->playSound(SoundPlayer::MessageReceived);
+void SoundEventController::handleEventQueueEventAdded(boost::shared_ptr<StanzaEvent> /*event*/) {
+	// message received sound is now played via highlighting
+	//if (playSounds_ && !event->getConcluded()) {
+	//	soundPlayer_->playSound(SoundPlayer::MessageReceived);
+	//}
+}
+
+void SoundEventController::handleHighlight(const HighlightAction& action) {
+	if (playSounds_ && action.playSound()) {
+		soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFile());
 	}
 }
 
diff --git a/Swift/Controllers/SoundEventController.h b/Swift/Controllers/SoundEventController.h
index 842125d..c9dcab4 100644
--- a/Swift/Controllers/SoundEventController.h
+++ b/Swift/Controllers/SoundEventController.h
@@ -10,21 +10,25 @@
 
 #include <Swift/Controllers/XMPPEvents/StanzaEvent.h>
 #include <Swift/Controllers/Settings/SettingsProvider.h>
+#include <Swift/Controllers/HighlightAction.h>
 
 namespace Swift {
 	class EventController;
 	class SoundPlayer;
+	class HighlightManager;
 	class SoundEventController {
 		public:
-			SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings);
+			SoundEventController(EventController* eventController, SoundPlayer* soundPlayer, SettingsProvider* settings, HighlightManager* highlightManager);
 			void setPlaySounds(bool playSounds);
 			bool getSoundEnabled() {return playSounds_;}
 		private:
 			void handleSettingChanged(const std::string& settingPath);
 			void handleEventQueueEventAdded(boost::shared_ptr<StanzaEvent> event);
+			void handleHighlight(const HighlightAction& action);
 			EventController* eventController_;
 			SoundPlayer* soundPlayer_;
 			bool playSounds_;
 			SettingsProvider* settings_;
+			HighlightManager* highlightManager_;
 	};
 }
diff --git a/Swift/Controllers/SoundPlayer.h b/Swift/Controllers/SoundPlayer.h
index 19bf8b6..f18a2c0 100644
--- a/Swift/Controllers/SoundPlayer.h
+++ b/Swift/Controllers/SoundPlayer.h
@@ -6,11 +6,13 @@
 
 #pragma once
 
+#include <string>
+
 namespace Swift {
 	class SoundPlayer {
 		public:
 			virtual ~SoundPlayer() {}
 			enum SoundEffect{MessageReceived};
-			virtual void playSound(SoundEffect sound) = 0;
+			virtual void playSound(SoundEffect sound, const std::string& soundResource) = 0;
 	};
 }
diff --git a/Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h b/Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h
new file mode 100644
index 0000000..42e22a2
--- /dev/null
+++ b/Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+
+namespace Swift {
+
+	class RequestHighlightEditorUIEvent : public UIEvent {
+	};
+
+}
diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h
index ad0ed15..252e43d 100644
--- a/Swift/Controllers/UIInterfaces/ChatWindow.h
+++ b/Swift/Controllers/UIInterfaces/ChatWindow.h
@@ -17,6 +17,7 @@
 #include <Swiften/Elements/ChatState.h>
 #include <Swiften/Elements/Form.h>
 #include <Swiften/Elements/MUCOccupant.h>
+#include <Swift/Controllers/HighlightManager.h>
 
 
 namespace Swift {
@@ -44,16 +45,16 @@ namespace Swift {
 			/** Add message to window.
 			 * @return id of added message (for acks).
 			 */
-			virtual std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) = 0;
+			virtual std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0;
 			/** Adds action to window.
 			 * @return id of added message (for acks);
 			 */
-			virtual std::string addAction(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) = 0;
+			virtual std::string addAction(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0;
 			virtual void addSystemMessage(const std::string& message) = 0;
 			virtual void addPresenceMessage(const std::string& message) = 0;
 			virtual void addErrorMessage(const std::string& message) = 0;
-			virtual void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) = 0;
-			virtual void replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) = 0;
+			virtual void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0;
+			virtual void replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0;
 			
 			// File transfer related stuff
 			virtual std::string addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) = 0;
diff --git a/Swift/Controllers/UIInterfaces/HighlightEditorWidget.h b/Swift/Controllers/UIInterfaces/HighlightEditorWidget.h
new file mode 100644
index 0000000..4745035
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/HighlightEditorWidget.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+namespace Swift {
+
+	class HighlightManager;
+
+	class HighlightEditorWidget {
+		public:
+			virtual ~HighlightEditorWidget() {}
+
+			virtual void show() = 0;
+
+			virtual void setHighlightManager(HighlightManager* highlightManager) = 0;
+	};
+
+}
diff --git a/Swift/Controllers/UIInterfaces/HighlightEditorWidgetFactory.h b/Swift/Controllers/UIInterfaces/HighlightEditorWidgetFactory.h
new file mode 100644
index 0000000..ade575b
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/HighlightEditorWidgetFactory.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+namespace Swift {
+
+	class HighlightEditorWidget;
+
+	class HighlightEditorWidgetFactory {
+		public:
+			virtual ~HighlightEditorWidgetFactory() {}
+
+			virtual HighlightEditorWidget* createHighlightEditorWidget() = 0;
+	};
+
+}
diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h
index 6b4efd8..dcd1779 100644
--- a/Swift/Controllers/UIInterfaces/UIFactory.h
+++ b/Swift/Controllers/UIInterfaces/UIFactory.h
@@ -21,6 +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>
 
 namespace Swift {
 	class UIFactory : 
@@ -38,7 +39,8 @@ namespace Swift {
 			public ContactEditWindowFactory,
 			public AdHocCommandWindowFactory,
 			public FileTransferListWidgetFactory,
-			public WhiteboardWindowFactory {
+			public WhiteboardWindowFactory,
+			public HighlightEditorWidgetFactory {
 		public:
 			virtual ~UIFactory() {}
 	};
diff --git a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp
new file mode 100644
index 0000000..ec81227
--- /dev/null
+++ b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp
@@ -0,0 +1,318 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <vector>
+#include <string>
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include <Swift/Controllers/HighlightRule.h>
+
+using namespace Swift;
+
+class HighlightRuleTest : public CppUnit::TestFixture {
+		CPPUNIT_TEST_SUITE(HighlightRuleTest);
+		CPPUNIT_TEST(testEmptyRuleNeverMatches);
+		CPPUNIT_TEST(testKeyword);
+		CPPUNIT_TEST(testNickKeyword);
+		CPPUNIT_TEST(testNickWithoutOtherKeywords);
+		CPPUNIT_TEST(testSender);
+		CPPUNIT_TEST(testSenderAndKeyword);
+		CPPUNIT_TEST(testWholeWords);
+		CPPUNIT_TEST(testCase);
+		CPPUNIT_TEST(testWholeWordsAndCase);
+		CPPUNIT_TEST(testMUC);
+		CPPUNIT_TEST_SUITE_END();
+
+	public:
+		void setUp() {
+			std::vector<std::string> keywords;
+			keywords.push_back("keyword1");
+			keywords.push_back("KEYWORD2");
+
+			std::vector<std::string>senders;
+			senders.push_back("sender1");
+			senders.push_back("SENDER2");
+
+			emptyRule = new HighlightRule();
+
+			keywordRule = new HighlightRule();
+			keywordRule->setKeywords(keywords);
+
+			keywordChatRule = new HighlightRule();
+			keywordChatRule->setKeywords(keywords);
+			keywordChatRule->setMatchChat(true);
+
+			keywordNickChatRule = new HighlightRule();
+			keywordNickChatRule->setKeywords(keywords);
+			keywordNickChatRule->setNickIsKeyword(true);
+			keywordNickChatRule->setMatchChat(true);
+
+			nickChatRule = new HighlightRule();
+			nickChatRule->setNickIsKeyword(true);
+			nickChatRule->setMatchChat(true);
+
+			nickRule = new HighlightRule();
+			nickRule->setNickIsKeyword(true);
+
+			senderRule = new HighlightRule();
+			senderRule->setSenders(senders);
+
+			senderChatRule = new HighlightRule();
+			senderChatRule->setSenders(senders);
+			senderChatRule->setMatchChat(true);
+
+			senderKeywordChatRule = new HighlightRule();
+			senderKeywordChatRule->setSenders(senders);
+			senderKeywordChatRule->setKeywords(keywords);
+			senderKeywordChatRule->setMatchChat(true);
+
+			senderKeywordNickChatRule = new HighlightRule();
+			senderKeywordNickChatRule->setSenders(senders);
+			senderKeywordNickChatRule->setKeywords(keywords);
+			senderKeywordNickChatRule->setNickIsKeyword(true);
+			senderKeywordNickChatRule->setMatchChat(true);
+
+			senderKeywordNickWordChatRule = new HighlightRule();
+			senderKeywordNickWordChatRule->setSenders(senders);
+			senderKeywordNickWordChatRule->setKeywords(keywords);
+			senderKeywordNickWordChatRule->setNickIsKeyword(true);
+			senderKeywordNickWordChatRule->setMatchWholeWords(true);
+			senderKeywordNickWordChatRule->setMatchChat(true);
+
+			senderKeywordNickCaseChatRule = new HighlightRule();
+			senderKeywordNickCaseChatRule->setSenders(senders);
+			senderKeywordNickCaseChatRule->setKeywords(keywords);
+			senderKeywordNickCaseChatRule->setNickIsKeyword(true);
+			senderKeywordNickCaseChatRule->setMatchCase(true);
+			senderKeywordNickCaseChatRule->setMatchChat(true);
+
+			senderKeywordNickCaseWordChatRule = new HighlightRule();
+			senderKeywordNickCaseWordChatRule->setSenders(senders);
+			senderKeywordNickCaseWordChatRule->setKeywords(keywords);
+			senderKeywordNickCaseWordChatRule->setNickIsKeyword(true);
+			senderKeywordNickCaseWordChatRule->setMatchCase(true);
+			senderKeywordNickCaseWordChatRule->setMatchWholeWords(true);
+			senderKeywordNickCaseWordChatRule->setMatchChat(true);
+
+			senderKeywordNickMUCRule = new HighlightRule();
+			senderKeywordNickMUCRule->setSenders(senders);
+			senderKeywordNickMUCRule->setKeywords(keywords);
+			senderKeywordNickMUCRule->setNickIsKeyword(true);
+			senderKeywordNickMUCRule->setMatchMUC(true);
+		}
+
+		void tearDown() {
+			delete emptyRule;
+
+			delete keywordRule;
+			delete keywordChatRule;
+			delete keywordNickChatRule;
+			delete nickChatRule;
+			delete nickRule;
+
+			delete senderRule;
+			delete senderChatRule;
+			delete senderKeywordChatRule;
+			delete senderKeywordNickChatRule;
+
+			delete senderKeywordNickWordChatRule;
+			delete senderKeywordNickCaseChatRule;
+			delete senderKeywordNickCaseWordChatRule;
+
+			delete senderKeywordNickMUCRule;
+		}
+
+		void testEmptyRuleNeverMatches() {
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::MUCMessage), false);
+		}
+
+		void testKeyword() {
+			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::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("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("keyword2", "from", "nick", HighlightRule::ChatMessage), true);
+		}
+
+		void testNickKeyword() {
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true);
+			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("body", "sender contains nick", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains mixed-case NiCk", "sender", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false);
+		}
+
+		void testNickWithoutOtherKeywords() {
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false);
+			CPPUNIT_ASSERT_EQUAL(nickRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "sender contains nick but it does't matter", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains mixed-case NiCk", "from", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true);
+
+			// there are no keywords in this rule and empty nick is not treated as a keyword, so we don't check for keywords to get a match
+			CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), true);
+		}
+
+		void testSender() {
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::MUCMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body contains sender1", "from", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc sender1 xyz", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcsender1xyz", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "SENDer1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc SENDer1 xyz", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcSENDer1xyz", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender2", "nick", HighlightRule::ChatMessage), true);
+		}
+
+		void testSenderAndKeyword() {
+			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);
+		}
+
+		void testWholeWords() {
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
+			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("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("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), true);
+		}
+
+		void testCase() {
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
+			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("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), true);
+
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false);
+		}
+
+		void testWholeWordsAndCase() {
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false);
+			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("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false);
+
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false);
+			CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false);
+		}
+
+		void testMUC() {
+			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("body contains nick", "sender1", "nick", HighlightRule::MUCMessage), true);
+		}
+
+	private:
+		HighlightRule* emptyRule;
+
+		HighlightRule* keywordRule;
+		HighlightRule* keywordChatRule;
+		HighlightRule* keywordNickChatRule;
+		HighlightRule* nickChatRule;
+		HighlightRule* nickRule;
+
+		HighlightRule* senderRule;
+		HighlightRule* senderChatRule;
+		HighlightRule* senderKeywordChatRule;
+		HighlightRule* senderKeywordNickChatRule;
+
+		HighlightRule* senderKeywordNickWordChatRule;
+		HighlightRule* senderKeywordNickCaseChatRule;
+		HighlightRule* senderKeywordNickCaseWordChatRule;
+
+		HighlightRule* senderKeywordNickMUCRule;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(HighlightRuleTest);
diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h
index ac3f21b..84aaa04 100644
--- a/Swift/Controllers/UnitTest/MockChatWindow.h
+++ b/Swift/Controllers/UnitTest/MockChatWindow.h
@@ -14,8 +14,8 @@ namespace Swift {
 			MockChatWindow() : labelsEnabled_(false) {}
 			virtual ~MockChatWindow();
 
-			virtual std::string addMessage(const std::string& message, const std::string& /*senderName*/, bool /*senderIsSelf*/, boost::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message; return "";}
-			virtual std::string addAction(const std::string& message, const std::string& /*senderName*/, bool /*senderIsSelf*/, boost::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message; return "";}
+			virtual std::string addMessage(const std::string& message, const std::string& /*senderName*/, bool /*senderIsSelf*/, boost::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime&, const HighlightAction&) {lastMessageBody_ = message; return "";}
+			virtual std::string addAction(const std::string& message, const std::string& /*senderName*/, bool /*senderIsSelf*/, boost::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime&, const HighlightAction&) {lastMessageBody_ = message; return "";}
 			virtual void addSystemMessage(const std::string& /*message*/) {}
 			virtual void addErrorMessage(const std::string& /*message*/) {}
 			virtual void addPresenceMessage(const std::string& /*message*/) {}
@@ -41,8 +41,8 @@ namespace Swift {
 			virtual void setRosterModel(Roster* /*roster*/) {}
 			virtual void setTabComplete(TabComplete*) {}
 			virtual void replaceLastMessage(const std::string&) {}
-			virtual void replaceMessage(const std::string&, const std::string&, const boost::posix_time::ptime&) {}
-			virtual void replaceWithAction(const std::string& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/) {}
+			virtual void replaceMessage(const std::string&, const std::string&, const boost::posix_time::ptime&, const HighlightAction&) {}
+			virtual void replaceWithAction(const std::string& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/, const HighlightAction&) {}
 			void setAckState(const std::string& /*id*/, AckState /*state*/) {}
 			virtual void flash() {}
 			virtual void setAlert(const std::string& /*alertText*/, const std::string& /*buttonText*/) {}
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 5d57184..a53ca5d 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -481,8 +481,8 @@ void QtChatWindow::updateTitleWithUnreadCount() {
 	emit titleUpdated();
 }
 
-std::string QtChatWindow::addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) {
-	return addMessage(linkimoticonify(P2QSTRING(message)), senderName, senderIsSelf, label, avatarPath, "", time);
+std::string QtChatWindow::addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
+	return addMessage(linkimoticonify(P2QSTRING(message)), senderName, senderIsSelf, label, avatarPath, "", time, highlight);
 }
 
 QString QtChatWindow::linkimoticonify(const QString& message) const {
@@ -502,7 +502,21 @@ QString QtChatWindow::linkimoticonify(const QString& message) const {
 	return messageHTML;
 }
 
-std::string QtChatWindow::addMessage(const QString &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time) {
+QString QtChatWindow::getHighlightSpanStart(const HighlightAction& highlight)
+{
+	QString color = Qt::escape(P2QSTRING(highlight.getTextColor()));
+	QString background = Qt::escape(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 QtChatWindow::addMessage(const QString &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
 	if (isWidgetSelected()) {
 		onAllMessagesRead();
 	}
@@ -516,7 +530,9 @@ std::string QtChatWindow::addMessage(const QString &message, const std::string &
 	QString messageHTML(message);
 	QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">";
 	QString styleSpanEnd = style == "" ? "" : "</span>";
-	htmlString += "<span class='swift_inner_message'>" + styleSpanStart + messageHTML + styleSpanEnd + "</span>" ;
+	QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : "";
+	QString highlightSpanEnd = highlight.highlightText() ? "</span>" : "";
+	htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd + "</span>" ;
 
 	bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf);
 	if (lastLineTracker_.getShouldMoveLastLine()) {
@@ -572,8 +588,8 @@ int QtChatWindow::getCount() {
 	return unreadCount_;
 }
 
-std::string QtChatWindow::addAction(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) {
-	return addMessage(" *" + linkimoticonify(P2QSTRING(message)) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time);
+std::string QtChatWindow::addAction(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
+	return addMessage(" *" + linkimoticonify(P2QSTRING(message)) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, highlight);
 }
 
 // FIXME: Move this to a different file
@@ -770,15 +786,15 @@ void QtChatWindow::addSystemMessage(const std::string& message) {
 	previousMessageKind_ = PreviousMessageWasSystem;
 }
 
-void QtChatWindow::replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) {
-	replaceMessage(" *" + linkimoticonify(P2QSTRING(message)) + "*", id, time, "font-style:italic ");
+void QtChatWindow::replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
+	replaceMessage(" *" + linkimoticonify(P2QSTRING(message)) + "*", id, time, "font-style:italic ", highlight);
 }
 
-void QtChatWindow::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) {
-	replaceMessage(linkimoticonify(P2QSTRING(message)), id, time, "");
+void QtChatWindow::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) {
+	replaceMessage(linkimoticonify(P2QSTRING(message)), id, time, "", highlight);
 }
 
-void QtChatWindow::replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style) {
+void QtChatWindow::replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style, const HighlightAction& highlight) {
 	if (!id.empty()) {
 		if (isWidgetSelected()) {
 			onAllMessagesRead();
@@ -788,7 +804,9 @@ void QtChatWindow::replaceMessage(const QString& message, const std::string& id,
 
 		QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">";
 		QString styleSpanEnd = style == "" ? "" : "</span>";
-		messageHTML = styleSpanStart + messageHTML + styleSpanEnd;
+		QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : "";
+		QString highlightSpanEnd = highlight.highlightText() ? "</span>" : "";
+		messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd;
 
 		messageLog_->replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time));
 	}
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index c32ae83..4abd456 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -88,13 +88,13 @@ namespace Swift {
 		public:
 			QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings, QMap<QString, QString> emoticons);
 			~QtChatWindow();
-			std::string addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time);
-			std::string addAction(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time);
+			std::string addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight);
+			std::string addAction(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight);
 			void addSystemMessage(const std::string& message);
 			void addPresenceMessage(const std::string& message);
 			void addErrorMessage(const std::string& errorMessage);
-			void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time);
-			void replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time);
+			void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight);
+			void replaceWithAction(const std::string& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight);
 			// File transfer related stuff
 			std::string addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes);
 			void setFileTransferProgress(std::string id, const int percentageDone);
@@ -192,11 +192,12 @@ namespace Swift {
 			void beginCorrection();
 			void cancelCorrection();
 			void handleSettingChanged(const std::string& setting);
-			std::string addMessage(const QString& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time);
-			void replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style);
+			std::string addMessage(const QString& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time, const HighlightAction& highlight);
+			void replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style, const HighlightAction& highlight);
 			void handleOccupantSelectionChanged(RosterItem* item);
 			bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf) const;
 			QString linkimoticonify(const QString& message) const;
+			QString getHighlightSpanStart(const HighlightAction& highlight);
 
 			int unreadCount_;
 			bool contactIsTyping_;
diff --git a/Swift/QtUI/QtColorToolButton.cpp b/Swift/QtUI/QtColorToolButton.cpp
new file mode 100644
index 0000000..1d379a3
--- /dev/null
+++ b/Swift/QtUI/QtColorToolButton.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <QColorDialog>
+#include <QPainter>
+
+#include <Swift/QtUI/QtColorToolButton.h>
+
+namespace Swift {
+
+QtColorToolButton::QtColorToolButton(QWidget* parent) :
+	QToolButton(parent)
+{
+	connect(this, SIGNAL(clicked()), SLOT(onClicked()));
+	setColorIcon(Qt::transparent);
+}
+
+void QtColorToolButton::setColor(const QColor& color)
+{
+	if (color.isValid() != color_.isValid() || (color.isValid() && color != color_)) {
+		color_ = color;
+		setColorIcon(color_);
+		emit colorChanged(color_);
+	}
+}
+
+void QtColorToolButton::onClicked()
+{
+	QColor c = QColorDialog::getColor(color_, this);
+	if (c.isValid()) {
+		setColor(c);
+	}
+}
+
+void QtColorToolButton::setColorIcon(const QColor& color)
+{
+	QPixmap pix(iconSize());
+	pix.fill(color.isValid() ? color : Qt::transparent);
+	setIcon(pix);
+}
+
+}
diff --git a/Swift/QtUI/QtColorToolButton.h b/Swift/QtUI/QtColorToolButton.h
new file mode 100644
index 0000000..33d195d
--- /dev/null
+++ b/Swift/QtUI/QtColorToolButton.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <QToolButton>
+
+namespace Swift {
+
+	class QtColorToolButton : public QToolButton {
+		Q_OBJECT
+		Q_PROPERTY(QColor color READ getColor WRITE setColor NOTIFY colorChanged)
+		public:
+			explicit QtColorToolButton(QWidget* parent = NULL);
+			void setColor(const QColor& color);
+			const QColor& getColor() const { return color_; }
+
+		signals:
+			void colorChanged(const QColor&);
+
+		private slots:
+			void onClicked();
+
+		private:
+			void setColorIcon(const QColor& color);
+			QColor color_;
+	};
+
+}
diff --git a/Swift/QtUI/QtHighlightEditorWidget.cpp b/Swift/QtUI/QtHighlightEditorWidget.cpp
new file mode 100644
index 0000000..7ff094e
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditorWidget.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <cassert>
+
+#include <Swift/QtUI/QtHighlightEditorWidget.h>
+#include <Swift/QtUI/QtHighlightRulesItemModel.h>
+
+namespace Swift {
+
+QtHighlightEditorWidget::QtHighlightEditorWidget(QWidget* parent)
+	: QWidget(parent)
+{
+	ui_.setupUi(this);
+
+	itemModel_ = new QtHighlightRulesItemModel(this);
+	ui_.treeView->setModel(itemModel_);
+	ui_.ruleWidget->setModel(itemModel_);
+
+	for (int i = 0; i < QtHighlightRulesItemModel::NumberOfColumns; ++i) {
+		switch (i) {
+			case QtHighlightRulesItemModel::ApplyTo:
+			case QtHighlightRulesItemModel::Sender:
+			case QtHighlightRulesItemModel::Keyword:
+			case QtHighlightRulesItemModel::Action:
+				ui_.treeView->showColumn(i);
+				break;
+			default:
+				ui_.treeView->hideColumn(i);
+				break;
+		}
+	}
+
+	setHighlightManager(NULL); // setup buttons for empty rules list
+
+	connect(ui_.treeView->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), SLOT(onCurrentRowChanged(QModelIndex)));
+
+	connect(ui_.newButton, SIGNAL(clicked()), SLOT(onNewButtonClicked()));
+	connect(ui_.deleteButton, SIGNAL(clicked()), SLOT(onDeleteButtonClicked()));
+
+	connect(ui_.moveUpButton, SIGNAL(clicked()), SLOT(onMoveUpButtonClicked()));
+	connect(ui_.moveDownButton, SIGNAL(clicked()), SLOT(onMoveDownButtonClicked()));
+
+	connect(ui_.closeButton, SIGNAL(clicked()), SLOT(close()));
+
+	setWindowTitle(tr("Highlight Rules"));
+}
+
+QtHighlightEditorWidget::~QtHighlightEditorWidget()
+{
+}
+
+void QtHighlightEditorWidget::show()
+{
+	if (itemModel_->rowCount(QModelIndex())) {
+		selectRow(0);
+	}
+	QWidget::show();
+	QWidget::activateWindow();
+}
+
+void QtHighlightEditorWidget::setHighlightManager(HighlightManager* highlightManager)
+{
+	itemModel_->setHighlightManager(highlightManager);
+	ui_.newButton->setEnabled(highlightManager != NULL);
+
+	ui_.ruleWidget->setEnabled(false);
+	ui_.deleteButton->setEnabled(false);
+	ui_.moveUpButton->setEnabled(false);
+	ui_.moveDownButton->setEnabled(false);
+}
+
+void QtHighlightEditorWidget::closeEvent(QCloseEvent* event) {
+	ui_.ruleWidget->save();
+	event->accept();
+}
+
+void QtHighlightEditorWidget::onNewButtonClicked()
+{
+	int row = getSelectedRow() + 1;
+	itemModel_->insertRow(row, QModelIndex());
+	selectRow(row);
+}
+
+void QtHighlightEditorWidget::onDeleteButtonClicked()
+{
+	int row = getSelectedRow();
+	assert(row >= 0);
+
+	itemModel_->removeRow(row, QModelIndex());
+	if (row == itemModel_->rowCount(QModelIndex())) {
+		--row;
+	}
+	selectRow(row);
+}
+
+void QtHighlightEditorWidget::onMoveUpButtonClicked()
+{
+	int row = getSelectedRow();
+	assert(row > 0);
+
+	ui_.ruleWidget->save();
+	ui_.ruleWidget->setActiveIndex(QModelIndex());
+	itemModel_->swapRows(row, row - 1);
+	selectRow(row - 1);
+}
+
+void QtHighlightEditorWidget::onMoveDownButtonClicked()
+{
+	int row = getSelectedRow();
+	assert(row < itemModel_->rowCount(QModelIndex()) - 1);
+
+	ui_.ruleWidget->save();
+	ui_.ruleWidget->setActiveIndex(QModelIndex());
+	if (itemModel_->swapRows(row, row + 1)) {
+		selectRow(row + 1);
+	}
+}
+
+void QtHighlightEditorWidget::onCurrentRowChanged(const QModelIndex& index)
+{
+	ui_.ruleWidget->save();
+	ui_.ruleWidget->setActiveIndex(index);
+
+	ui_.ruleWidget->setEnabled(index.isValid());
+
+	ui_.deleteButton->setEnabled(index.isValid());
+
+	ui_.moveUpButton->setEnabled(index.isValid() && index.row() != 0);
+	ui_.moveDownButton->setEnabled(index.isValid() && index.row() != itemModel_->rowCount(QModelIndex()) - 1);
+}
+
+void QtHighlightEditorWidget::selectRow(int row)
+{
+	QModelIndex index = itemModel_->index(row, 0, QModelIndex());
+	ui_.treeView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+}
+
+/** Return index of selected row or -1 if none is selected */
+int QtHighlightEditorWidget::getSelectedRow() const
+{
+	QModelIndexList rows = ui_.treeView->selectionModel()->selectedRows();
+	return rows.isEmpty() ? -1 : rows[0].row();
+}
+
+}
diff --git a/Swift/QtUI/QtHighlightEditorWidget.h b/Swift/QtUI/QtHighlightEditorWidget.h
new file mode 100644
index 0000000..1293c87
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditorWidget.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <Swift/Controllers/UIInterfaces/HighlightEditorWidget.h>
+#include <Swift/QtUI/ui_QtHighlightEditorWidget.h>
+
+namespace Swift {
+
+	class QtHighlightRulesItemModel;
+
+	class QtHighlightEditorWidget : public QWidget, public HighlightEditorWidget {
+		Q_OBJECT
+
+		public:
+			QtHighlightEditorWidget(QWidget* parent = NULL);
+			virtual ~QtHighlightEditorWidget();
+
+			void show();
+
+			void setHighlightManager(HighlightManager* highlightManager);
+
+		private slots:
+			void onNewButtonClicked();
+			void onDeleteButtonClicked();
+			void onMoveUpButtonClicked();
+			void onMoveDownButtonClicked();
+			void onCurrentRowChanged(const QModelIndex&);
+
+		private:
+			virtual void closeEvent(QCloseEvent* event);
+
+			void selectRow(int row);
+			int getSelectedRow() const;
+
+			Ui::QtHighlightEditorWidget ui_;
+			QtHighlightRulesItemModel* itemModel_;
+		};
+
+}
diff --git a/Swift/QtUI/QtHighlightEditorWidget.ui b/Swift/QtUI/QtHighlightEditorWidget.ui
new file mode 100644
index 0000000..0f39168
--- /dev/null
+++ b/Swift/QtUI/QtHighlightEditorWidget.ui
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtHighlightEditorWidget</class>
+ <widget class="QWidget" name="QtHighlightEditorWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>419</width>
+    <height>373</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_2">
+     <item>
+      <widget class="QLabel" name="label">
+       <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="QTreeView" name="treeView">
+       <property name="rootIsDecorated">
+        <bool>false</bool>
+       </property>
+       <property name="itemsExpandable">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="Swift::QtHighlightRuleWidget" name="ruleWidget" native="true"/>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout">
+     <item>
+      <widget class="QPushButton" name="newButton">
+       <property name="text">
+        <string>New</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="deleteButton">
+       <property name="text">
+        <string>Delete</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="verticalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeType">
+        <enum>QSizePolicy::Fixed</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </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>
+     <item>
+      <spacer name="verticalSpacer_3">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="closeButton">
+       <property name="text">
+        <string>Close</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>Swift::QtHighlightRuleWidget</class>
+   <extends>QWidget</extends>
+   <header>QtHighlightRuleWidget.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/Swift/QtUI/QtHighlightRuleWidget.cpp b/Swift/QtUI/QtHighlightRuleWidget.cpp
new file mode 100644
index 0000000..9c0df5e
--- /dev/null
+++ b/Swift/QtUI/QtHighlightRuleWidget.cpp
@@ -0,0 +1,134 @@
+/*
+ * 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
new file mode 100644
index 0000000..8a59a14
--- /dev/null
+++ b/Swift/QtUI/QtHighlightRuleWidget.h
@@ -0,0 +1,49 @@
+/*
+ * 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
new file mode 100644
index 0000000..9c465f9
--- /dev/null
+++ b/Swift/QtUI/QtHighlightRuleWidget.ui
@@ -0,0 +1,260 @@
+<?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/QtHighlightRulesItemModel.cpp b/Swift/QtUI/QtHighlightRulesItemModel.cpp
new file mode 100644
index 0000000..ff2f639
--- /dev/null
+++ b/Swift/QtUI/QtHighlightRulesItemModel.cpp
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <boost/algorithm/string.hpp>
+#include <boost/lambda/lambda.hpp>
+#include <boost/numeric/conversion/cast.hpp>
+
+#include <QStringList>
+#include <QColor>
+
+#include <Swift/Controllers/HighlightManager.h>
+#include <Swift/QtUI/QtHighlightRulesItemModel.h>
+#include <Swift/QtUI/QtSwiftUtil.h>
+
+namespace Swift {
+
+QtHighlightRulesItemModel::QtHighlightRulesItemModel(QObject* parent) : QAbstractItemModel(parent), highlightManager_(NULL)
+{
+}
+
+void QtHighlightRulesItemModel::setHighlightManager(HighlightManager* highlightManager)
+{
+	emit layoutAboutToBeChanged();
+	highlightManager_ = highlightManager;
+	emit layoutChanged();
+}
+
+QVariant QtHighlightRulesItemModel::headerData(int section, Qt::Orientation /* orientation */, int role) const
+{
+	if (role == Qt::DisplayRole) {
+		switch (section) {
+			case ApplyTo: return QVariant(tr("Apply to"));
+			case Sender: return QVariant(tr("Sender"));
+			case Keyword: return QVariant(tr("Keyword"));
+			case Action: return QVariant(tr("Action"));
+			case NickIsKeyword: return QVariant(tr("Nick Is Keyword"));
+			case MatchCase: return QVariant(tr("Match Case"));
+			case MatchWholeWords: return QVariant(tr("Match Whole Words"));
+			case HighlightText: return QVariant(tr("Highlight Text"));
+			case TextColor: return QVariant(tr("Text Color"));
+			case TextBackground: return QVariant(tr("Text Background"));
+			case PlaySound: return QVariant(tr("Play Sounds"));
+			case SoundFile: return QVariant(tr("Sound File"));
+		}
+	}
+
+	return QVariant();
+}
+
+int QtHighlightRulesItemModel::columnCount(const QModelIndex& /* parent */) const
+{
+	return NumberOfColumns;
+}
+
+QVariant QtHighlightRulesItemModel::data(const QModelIndex &index, int role) const
+{
+	if (index.isValid() && highlightManager_ && (role == Qt::DisplayRole || role == Qt::EditRole)) {
+
+		const char* separator = (role == Qt::DisplayRole) ? " ; " : "\n";
+
+		if (boost::numeric_cast<std::vector<std::string>::size_type>(index.row()) < highlightManager_->getRules().size()) {
+			const HighlightRule& r = highlightManager_->getRules()[index.row()];
+			switch (index.column()) {
+				case ApplyTo: {
+					int applyTo = 0;
+					if (r.getMatchChat() && r.getMatchMUC()) {
+						applyTo = 1;
+					} else if (r.getMatchChat()) {
+						applyTo = 2;
+					} else if (r.getMatchMUC()) {
+						applyTo = 3;
+					}
+
+					if (role == Qt::DisplayRole) {
+						return QVariant(getApplyToString(applyTo));
+					} else {
+						return QVariant(applyTo);
+					}
+				}
+				case Sender: {
+					std::string s = boost::join(r.getSenders(), separator);
+					return QVariant(P2QSTRING(s));
+				}
+				case Keyword: {
+					std::string s = boost::join(r.getKeywords(), separator);
+					QString qs(P2QSTRING(s));
+					if (role == Qt::DisplayRole && r.getNickIsKeyword()) {
+						qs = tr("<nick>") + (qs.isEmpty() ? "" : separator + qs);
+					}
+					return QVariant(qs);
+				}
+				case Action: {
+					std::vector<std::string> v;
+					const HighlightAction & action = r.getAction();
+					if (action.highlightText()) {
+						v.push_back(Q2PSTRING(tr("Highlight text")));
+					}
+					if (action.playSound()) {
+						v.push_back(Q2PSTRING(tr("Play sound")));
+					}
+					std::string s = boost::join(v, separator);
+					return QVariant(P2QSTRING(s));
+				}
+				case NickIsKeyword: {
+					return QVariant(r.getNickIsKeyword());
+				}
+				case MatchCase: {
+					return QVariant(r.getMatchCase());
+				}
+				case MatchWholeWords: {
+					return QVariant(r.getMatchWholeWords());
+				}
+				case HighlightText: {
+					return QVariant(r.getAction().highlightText());
+				}
+				case TextColor: {
+					return QVariant(QColor(P2QSTRING(r.getAction().getTextColor())));
+				}
+				case TextBackground: {
+					return QVariant(QColor(P2QSTRING(r.getAction().getTextBackground())));
+				}
+				case PlaySound: {
+					return QVariant(r.getAction().playSound());
+				}
+				case SoundFile: {
+					return QVariant(P2QSTRING(r.getAction().getSoundFile()));
+				}
+			}
+		}
+	}
+	return QVariant();
+}
+
+bool QtHighlightRulesItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+	if (index.isValid() && highlightManager_ && role == Qt::EditRole) {
+		if (boost::numeric_cast<std::vector<std::string>::size_type>(index.row()) < highlightManager_->getRules().size()) {
+			HighlightRule r = highlightManager_->getRule(index.row());
+			std::vector<int> changedColumns;
+			switch (index.column()) {
+				case ApplyTo: {
+					bool ok = false;
+					int applyTo = value.toInt(&ok);
+					if (!ok) {
+						return false;
+					}
+					r.setMatchChat(applyTo == ApplyToAll || applyTo == ApplyToChat);
+					r.setMatchMUC(applyTo == ApplyToAll || applyTo == ApplyToMUC);
+					break;
+				}
+				case Sender:
+				case Keyword: {
+					std::string s = Q2PSTRING(value.toString());
+					std::vector<std::string> v;
+					boost::split(v, s, boost::is_any_of("\n"));
+					v.erase(std::remove_if(v.begin(), v.end(), boost::lambda::_1 == ""), v.end());
+					if (index.column() == Sender) {
+						r.setSenders(v);
+					} else {
+						r.setKeywords(v);
+					}
+					break;
+				}
+				case NickIsKeyword: {
+					r.setNickIsKeyword(value.toBool());
+					changedColumns.push_back(Keyword);	// "<nick>"
+					break;
+				}
+				case MatchCase: {
+					r.setMatchCase(value.toBool());
+					break;
+				}
+				case MatchWholeWords: {
+					r.setMatchWholeWords(value.toBool());
+					break;
+				}
+				case HighlightText: {
+					r.getAction().setHighlightText(value.toBool());
+					changedColumns.push_back(Action);
+					break;
+				}
+				case TextColor: {
+					QColor c = value.value<QColor>();
+					r.getAction().setTextColor(c.isValid() ? Q2PSTRING(c.name()) : "");
+					break;
+				}
+				case TextBackground: {
+					QColor c = value.value<QColor>();
+					r.getAction().setTextBackground(c.isValid() ? Q2PSTRING(c.name()) : "");
+					break;
+				}
+				case PlaySound: {
+					r.getAction().setPlaySound(value.toBool());
+					changedColumns.push_back(Action);
+					break;
+				}
+				case SoundFile: {
+					r.getAction().setSoundFile(Q2PSTRING(value.toString()));
+					break;
+				}
+			}
+
+			highlightManager_->setRule(index.row(), r);
+			emit dataChanged(index, index);
+			foreach (int column, changedColumns) {
+				QModelIndex i = createIndex(index.row(), column, 0);
+				emit dataChanged(i, i);
+			}
+		}
+	}
+
+	return false;
+}
+
+QModelIndex QtHighlightRulesItemModel::parent(const QModelIndex& /* child */) const
+{
+	return QModelIndex();
+}
+
+int QtHighlightRulesItemModel::rowCount(const QModelIndex& /* parent */) const
+{
+	return highlightManager_ ? highlightManager_->getRules().size() : 0;
+}
+
+QModelIndex QtHighlightRulesItemModel::index(int row, int column, const QModelIndex& /* parent */) const
+{
+	return createIndex(row, column, 0);
+}
+
+bool QtHighlightRulesItemModel::insertRows(int row, int count, const QModelIndex& /* parent */)
+{
+	if (highlightManager_) {
+		beginInsertRows(QModelIndex(), row, row + count);
+		while (count--) {
+			highlightManager_->insertRule(row, HighlightRule());
+		}
+		endInsertRows();
+		return true;
+	}
+	return false;
+}
+
+bool QtHighlightRulesItemModel::removeRows(int row, int count, const QModelIndex& /* parent */)
+{
+	if (highlightManager_) {
+		beginRemoveRows(QModelIndex(), row, row + count);
+		while (count--) {
+			highlightManager_->removeRule(row);
+		}
+		endRemoveRows();
+		return true;
+	}
+	return false;
+}
+
+bool QtHighlightRulesItemModel::swapRows(int row1, int row2)
+{
+	if (highlightManager_) {
+		assert(row1 >= 0 && row2 >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(row1) < highlightManager_->getRules().size() && boost::numeric_cast<std::vector<std::string>::size_type>(row2) < highlightManager_->getRules().size());
+		HighlightRule r = highlightManager_->getRule(row1);
+		highlightManager_->setRule(row1, highlightManager_->getRule(row2));
+		highlightManager_->setRule(row2, r);
+		emit dataChanged(index(row1, 0, QModelIndex()), index(row1, 0, QModelIndex()));
+		emit dataChanged(index(row2, 0, QModelIndex()), index(row2, 0, QModelIndex()));
+		return true;
+	}
+	return false;
+}
+
+QString QtHighlightRulesItemModel::getApplyToString(int applyTo)
+{
+	switch (applyTo) {
+		case ApplyToNone: return tr("None");
+		case ApplyToAll: return tr("Chat or MUC");
+		case ApplyToChat: return tr("Chat");
+		case ApplyToMUC: return tr("MUC");
+		default: return "";
+	}
+}
+
+}
diff --git a/Swift/QtUI/QtHighlightRulesItemModel.h b/Swift/QtUI/QtHighlightRulesItemModel.h
new file mode 100644
index 0000000..ac85628
--- /dev/null
+++ b/Swift/QtUI/QtHighlightRulesItemModel.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2012 Maciej Niedzielski
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <QAbstractItemModel>
+
+namespace Swift {
+
+	class HighlightManager;
+
+	class QtHighlightRulesItemModel : public QAbstractItemModel {
+		Q_OBJECT
+
+		public:
+			QtHighlightRulesItemModel(QObject* parent = NULL);
+
+			void setHighlightManager(HighlightManager* highlightManager);
+
+			QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+			int columnCount(const QModelIndex& parent) const;
+			QVariant data(const QModelIndex& index, int role) const;
+			bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
+			QModelIndex parent(const QModelIndex& child) const;
+			int rowCount(const QModelIndex& parent) const;
+			QModelIndex index(int row, int column, const QModelIndex& parent) const;
+			bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex());
+			bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex());
+			bool swapRows(int row1, int row2);
+
+			static QString getApplyToString(int);
+
+			enum Columns {
+				ApplyTo = 0,
+				Sender,
+				Keyword,
+				Action,
+				NickIsKeyword,
+				MatchCase,
+				MatchWholeWords,
+				HighlightText,
+				TextColor,
+				TextBackground,
+				PlaySound,
+				SoundFile,
+				NumberOfColumns // end of list marker
+			};
+
+			enum ApplyToValues {
+				ApplyToNone = 0,
+				ApplyToAll,
+				ApplyToChat,
+				ApplyToMUC,
+				ApplyToEOL	// end of list marker
+			};
+
+		private:
+			HighlightManager* highlightManager_;
+	};
+
+}
diff --git a/Swift/QtUI/QtLoginWindow.cpp b/Swift/QtUI/QtLoginWindow.cpp
index c27edfb..cf22ad0 100644
--- a/Swift/QtUI/QtLoginWindow.cpp
+++ b/Swift/QtUI/QtLoginWindow.cpp
@@ -30,6 +30,7 @@
 #include <Swift/Controllers/UIEvents/UIEventStream.h>
 #include <Swift/Controllers/UIEvents/RequestXMLConsoleUIEvent.h>
 #include <Swift/Controllers/UIEvents/RequestFileTransferListUIEvent.h>
+#include <Swift/Controllers/UIEvents/RequestHighlightEditorUIEvent.h>
 #include <Swift/Controllers/Settings/SettingsProvider.h>
 #include <Swift/Controllers/SettingConstants.h>
 #include <Swift/QtUI/QtUISettingConstants.h>
@@ -190,6 +191,10 @@ QtLoginWindow::QtLoginWindow(UIEventStream* uiEventStream, SettingsProvider* set
 	generalMenu_->addAction(fileTransferOverviewAction_);
 #endif
 
+	highlightEditorAction_ = new QAction(tr("&Edit Highlight Rules"), this);
+	connect(highlightEditorAction_, SIGNAL(triggered()), SLOT(handleShowHighlightEditor()));
+	generalMenu_->addAction(highlightEditorAction_);
+
 	toggleSoundsAction_ = new QAction(tr("&Play Sounds"), this);
 	toggleSoundsAction_->setCheckable(true);
 	toggleSoundsAction_->setChecked(settings_->getSetting(SettingConstants::PLAY_SOUNDS));
@@ -438,6 +443,10 @@ void QtLoginWindow::handleShowFileTransferOverview() {
 	uiEventStream_->send(boost::make_shared<RequestFileTransferListUIEvent>());
 }
 
+void QtLoginWindow::handleShowHighlightEditor() {
+	uiEventStream_->send(boost::make_shared<RequestHighlightEditorUIEvent>());
+}
+
 void QtLoginWindow::handleToggleSounds(bool enabled) {
 	settings_->storeSetting(SettingConstants::PLAY_SOUNDS, enabled);
 }
diff --git a/Swift/QtUI/QtLoginWindow.h b/Swift/QtUI/QtLoginWindow.h
index c1966d8..7415fbf 100644
--- a/Swift/QtUI/QtLoginWindow.h
+++ b/Swift/QtUI/QtLoginWindow.h
@@ -62,6 +62,7 @@ namespace Swift {
 			void handleQuit();
 			void handleShowXMLConsole();
 			void handleShowFileTransferOverview();
+			void handleShowHighlightEditor();
 			void handleToggleSounds(bool enabled);
 			void handleToggleNotifications(bool enabled);
 			void handleAbout();
@@ -103,6 +104,7 @@ namespace Swift {
 			SettingsProvider* settings_;
 			QAction* xmlConsoleAction_;
 			QAction* fileTransferOverviewAction_;
+			QAction* highlightEditorAction_;
 			TimerFactory* timerFactory_;
 			ClientOptions currentOptions_;
 	};
diff --git a/Swift/QtUI/QtSoundPlayer.cpp b/Swift/QtUI/QtSoundPlayer.cpp
index 387c6f3..63f76f0 100644
--- a/Swift/QtUI/QtSoundPlayer.cpp
+++ b/Swift/QtUI/QtSoundPlayer.cpp
@@ -16,10 +16,11 @@ namespace Swift {
 QtSoundPlayer::QtSoundPlayer(ApplicationPathProvider* applicationPathProvider) : applicationPathProvider(applicationPathProvider) {
 }
 
-void QtSoundPlayer::playSound(SoundEffect sound) {
+void QtSoundPlayer::playSound(SoundEffect sound, const std::string& soundResource) {
+
 	switch (sound) {
 		case MessageReceived:
-			playSound("/sounds/message-received.wav");
+			playSound(soundResource.empty() ? "/sounds/message-received.wav" : soundResource);
 			break;
 	}
 }
@@ -29,6 +30,9 @@ void QtSoundPlayer::playSound(const std::string& soundResource) {
 	if (boost::filesystem::exists(resourcePath)) {
 		QSound::play(resourcePath.string().c_str());
 	}
+	else if (boost::filesystem::exists(soundResource)) {
+		QSound::play(soundResource.c_str());
+	}
 	else {
 		std::cerr << "Unable to find sound: " << soundResource << std::endl;
 	}
diff --git a/Swift/QtUI/QtSoundPlayer.h b/Swift/QtUI/QtSoundPlayer.h
index 6945f45..f8da392 100644
--- a/Swift/QtUI/QtSoundPlayer.h
+++ b/Swift/QtUI/QtSoundPlayer.h
@@ -19,7 +19,7 @@ namespace Swift {
 		public:
 			QtSoundPlayer(ApplicationPathProvider* applicationPathProvider);
 
-			void playSound(SoundEffect sound);
+			void playSound(SoundEffect sound, const std::string& soundResource);
 
 		private:
 			void playSound(const std::string& soundResource);
diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp
index 008d042..2ec2818 100644
--- a/Swift/QtUI/QtUIFactory.cpp
+++ b/Swift/QtUI/QtUIFactory.cpp
@@ -25,6 +25,7 @@
 #include "QtContactEditWindow.h"
 #include "QtAdHocCommandWindow.h"
 #include "QtFileTransferListWidget.h"
+#include <QtHighlightEditorWidget.h>
 #include "Whiteboard/QtWhiteboardWindow.h"
 #include <Swift/Controllers/Settings/SettingsProviderHierachy.h>
 #include <Swift/QtUI/QtUISettingConstants.h>
@@ -162,6 +163,10 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(boost::shared_ptr<Whiteboa
 	return new QtWhiteboardWindow(whiteboardSession);
 }
 
+HighlightEditorWidget* QtUIFactory::createHighlightEditorWidget() {
+	return new QtHighlightEditorWidget();
+}
+
 void QtUIFactory::createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command) {
 	new QtAdHocCommandWindow(command);
 }
diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h
index 4cf91ca..a1baa82 100644
--- a/Swift/QtUI/QtUIFactory.h
+++ b/Swift/QtUI/QtUIFactory.h
@@ -48,6 +48,7 @@ namespace Swift {
 			virtual ContactEditWindow* createContactEditWindow();
 			virtual FileTransferListWidget* createFileTransferListWidget();
 			virtual WhiteboardWindow* createWhiteboardWindow(boost::shared_ptr<WhiteboardSession> whiteboardSession);
+			virtual HighlightEditorWidget* createHighlightEditorWidget();
 			virtual void createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command);
 
 		private slots:
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index 607a8a6..cd0ed57 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -113,6 +113,10 @@ sources = [
     "QtContactEditWindow.cpp",
     "QtContactEditWidget.cpp",
     "QtSingleWindow.cpp",
+    "QtHighlightEditorWidget.cpp",
+    "QtHighlightRulesItemModel.cpp",
+    "QtHighlightRuleWidget.cpp",
+    "QtColorToolButton.cpp",
     "ChatSnippet.cpp",
     "MessageSnippet.cpp",
     "SystemMessageSnippet.cpp",
@@ -231,6 +235,8 @@ 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.Qrc("DefaultTheme.qrc")
 myenv.Qrc("Swift.qrc")
 
-- 
cgit v0.10.2-6-g49f6