diff options
| author | Maciej Niedzielski <machekku@uaznia.net> | 2012-12-21 19:58:24 (GMT) | 
|---|---|---|
| committer | Maciej Niedzielski <machekku@uaznia.net> | 2013-01-09 12:34:06 (GMT) | 
| commit | 4ed137080a3d80d20a2cead47f741e3dd2f2d42e (patch) | |
| tree | f030f0d9b8e61733de4e2bec9cef7715d380af8f | |
| parent | a8e2d82a1be5e94ac39523fc3e0606fcc261e913 (diff) | |
| download | swift-4ed137080a3d80d20a2cead47f741e3dd2f2d42e.zip swift-4ed137080a3d80d20a2cead47f741e3dd2f2d42e.tar.bz2 | |
Highlighting support
Change-Id: Ib6bd42cecff018998117bc1e7db279a62b3af434
License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details.
54 files changed, 2465 insertions, 78 deletions
| 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>&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>&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>&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 &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 &words</string> +        </property> +       </widget> +      </item> +      <item row="7" column="1"> +       <widget class="QCheckBox" name="matchCase"> +        <property name="text"> +         <string>Match &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>&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&olors:</string> +          </property> +         </widget> +        </item> +        <item> +         <widget class="Swift::QtColorToolButton" name="foreground"> +          <property name="enabled"> +           <bool>false</bool> +          </property> +          <property name="text"> +           <string>&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>&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>&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&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") | 
 Swift
 Swift