From 4ed137080a3d80d20a2cead47f741e3dd2f2d42e Mon Sep 17 00:00:00 2001 From: Maciej Niedzielski Date: Fri, 21 Dec 2012 20:58:24 +0100 Subject: Highlighting support Change-Id: Ib6bd42cecff018998117bc1e7db279a62b3af434 License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details. diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index 16b22fe..0ffef0c 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -32,7 +32,7 @@ #include #include #include - +#include #include 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 me } } -void ChatController::postHandleIncomingMessage(boost::shared_ptr messageEvent) { +void ChatController::postHandleIncomingMessage(boost::shared_ptr 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 = sentStanza->getPayload(); if (replace) { eraseIf(unackedStanzas_, PairSecondEquals, 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(), 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(), 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); void postSendMessage(const std::string &body, boost::shared_ptr sentStanza); void preHandleIncomingMessage(boost::shared_ptr messageEvent); - void postHandleIncomingMessage(boost::shared_ptr messageEvent); + void postHandleIncomingMessage(boost::shared_ptr messageEvent, const HighlightAction&); void preSendMessageRequest(boost::shared_ptr); std::string senderDisplayNameFromMessage(const JID& from); virtual boost::optional getMessageTimestamp(boost::shared_ptr) 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 #include #include +#include +#include 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 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 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 m } boost::shared_ptr 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())); chatWindow_->addErrorMessage(errorMessage); @@ -244,6 +248,11 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr m } onActivity(body); + // Highlight + if (!isIncomingMessageFromMe(message)) { + highlight = highlighter_->findAction(body, senderDisplayNameFromMessage(from)); + } + boost::shared_ptr replace = message->getPayload(); if (replace) { std::string body = message->getBody(); @@ -251,11 +260,11 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr m std::map::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 m chatWindow_->show(); chatWindow_->setUnreadMessageCount(boost::numeric_cast(unreadMessages_.size())); onUnreadCountChanged(); - postHandleIncomingMessage(messageEvent); + postHandleIncomingMessage(messageEvent, highlight); } std::string ChatControllerBase::getErrorMessage(boost::shared_ptr 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 #include +#include 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 info); void handleIncomingMessage(boost::shared_ptr message); - std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr 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 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) = 0; virtual void preHandleIncomingMessage(boost::shared_ptr) {} - virtual void postHandleIncomingMessage(boost::shared_ptr) {} + virtual void postHandleIncomingMessage(boost::shared_ptr, const HighlightAction&) {} virtual void preSendMessageRequest(boost::shared_ptr) {} virtual bool isFromContact(const JID& from); virtual boost::optional getMessageTimestamp(boost::shared_ptr) 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 #include #include +#include #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(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 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 mes } } -void MUCController::postHandleIncomingMessage(boost::shared_ptr messageEvent) { +void MUCController::postHandleIncomingMessage(boost::shared_ptr messageEvent, const HighlightAction& highlight) { boost::shared_ptr message = messageEvent->getStanza(); if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && messageTargetsMe(message) && !message->getPayload()) { 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(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(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 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& 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& 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 onUserLeft; boost::signal onUserJoined; @@ -62,7 +63,7 @@ namespace Swift { std::string senderDisplayNameFromMessage(const JID& from); boost::optional getMessageTimestamp(boost::shared_ptr message) const; void preHandleIncomingMessage(boost::shared_ptr); - void postHandleIncomingMessage(boost::shared_ptr); + void postHandleIncomingMessage(boost::shared_ptr, 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 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 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(stanzaChannel_, iqRouter_, directedPresenceSender_, mucJID_, mucRegistry_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); - controller_ = new MUCController (self_, muc_, boost::optional(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_); + controller_ = new MUCController (self_, muc_, boost::optional(), 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 + +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 + +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 + +#include +#include +#include +#include +#include + +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 rawEvent) +{ + boost::shared_ptr event = boost::dynamic_pointer_cast(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 + +#include + +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 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 + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* 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 HighlightManager::rulesFromString(const std::string& rulesString) +{ + std::vector rules; + std::string s(rulesString); + typedef boost::split_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(*it)); + if (!r.isEmpty()) { + rules.push_back(r); + } + } + return rules; +} + +std::vector HighlightManager::getDefaultRules() +{ + std::vector 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::size_type>(index) < rules_.size()); + return rules_[index]; +} + +void HighlightManager::setRule(int index, const HighlightRule& rule) +{ + assert(index >= 0 && boost::numeric_cast::size_type>(index) < rules_.size()); + rules_[index] = rule; + storeSettings(); +} + +void HighlightManager::insertRule(int index, const HighlightRule& rule) +{ + assert(index >= 0 && boost::numeric_cast::size_type>(index) <= rules_.size()); + rules_.insert(rules_.begin() + index, rule); + storeSettings(); +} + +void HighlightManager::removeRule(int index) +{ + assert(index >= 0 && boost::numeric_cast::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 +#include + +#include +#include + +namespace Swift { + + class SettingsProvider; + class Highlighter; + + class HighlightManager { + public: + HighlightManager(SettingsProvider* settings); + + Highlighter* createHighlighter(); + + const std::vector& 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 onHighlight; + + private: + void handleSettingChanged(const std::string& settingPath); + + std::string rulesToString() const; + static std::vector rulesFromString(const std::string&); + static std::vector getDefaultRules(); + + SettingsProvider* settings_; + bool storingSettings_; + void storeSettings(); + void loadSettings(); + + std::vector 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 +#include +#include + +#include +#include + +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 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 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& senders) +{ + senders_ = senders; + updateRegex(); +} + +void HighlightRule::setKeywords(const std::vector& 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 +#include + +#include + +#include + +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& getSenders() const { return senders_; } + void setSenders(const std::vector&); + + const std::vector& getKeywords() const { return keywords_; } + void setKeywords(const std::vector&); + + 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 senders_; + std::vector keywords_; + bool nickIsKeyword_; + + mutable std::vector senderRegex_; + mutable std::vector 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 +#include +#include + +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 + +#include + +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 #include #include +#include +#include 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 SettingConstants::LOGIN_AUTOMATICALLY = Se const SettingsProvider::Setting SettingConstants::SHOW_OFFLINE("showOffline", false); const SettingsProvider::Setting SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting SettingConstants::PLAY_SOUNDS("playSounds", true); +const SettingsProvider::Setting 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 SHOW_OFFLINE; static const SettingsProvider::Setting EXPANDED_ROSTER_GROUPS; static const SettingsProvider::Setting PLAY_SOUNDS; + static const SettingsProvider::Setting 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 #include #include +#include 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 event) { - if (playSounds_ && !event->getConcluded()) { - soundPlayer_->playSound(SoundPlayer::MessageReceived); +void SoundEventController::handleEventQueueEventAdded(boost::shared_ptr /*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 #include +#include 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 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 + 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 + +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 #include #include +#include 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 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 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 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 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 #include #include +#include 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 +#include + +#include +#include + +#include + +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 keywords; + keywords.push_back("keyword1"); + keywords.push_back("KEYWORD2"); + + std::vectorsenders; + 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 /*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 /*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 /*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 /*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 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 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 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("").arg(color).arg(background); +} + +std::string QtChatWindow::addMessage(const QString &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr 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 == "" ? "" : ""; QString styleSpanEnd = style == "" ? "" : ""; - htmlString += "" + styleSpanStart + messageHTML + styleSpanEnd + "" ; + QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = highlight.highlightText() ? "" : ""; + htmlString += "" + styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd + "" ; 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 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 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 == "" ? "" : ""; QString styleSpanEnd = style == "" ? "" : ""; - messageHTML = styleSpanStart + messageHTML + styleSpanEnd; + QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = highlight.highlightText() ? "" : ""; + 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 emoticons); ~QtChatWindow(); - std::string addMessage(const std::string &message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr 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 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 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 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 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 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 +#include + +#include + +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 + +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 + +#include +#include + +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 +#include + +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 @@ + + + QtHighlightEditorWidget + + + + 0 + 0 + 419 + 373 + + + + Form + + + + + + + + Incoming messages are checked against the following rules. First rule that matches will be executed. + + + true + + + + + + + false + + + false + + + + + + + + + + + + + + New + + + + + + + Delete + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Move up + + + + + + + Move down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Close + + + + + + + + + + Swift::QtHighlightRuleWidget + QWidget +
QtHighlightRuleWidget.h
+ 1 +
+
+ + +
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 +#include +#include + +#include +#include + +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 +#include + +#include + +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 @@ + + + QtHighlightRuleWidget + + + + 0 + 0 + 361 + 524 + + + + Form + + + + + + Rule conditions + + + + QFormLayout::ExpandingFieldsGrow + + + + + Choose when this rule should be applied. +If you want to provide more than one sender or keyword, input them in separate lines. + + + true + + + + + + + Qt::Horizontal + + + + + + + &Apply to: + + + applyTo + + + + + + + + + + &Senders: + + + senders + + + + + + + + + + &Keywords: + + + keywords + + + + + + + + + + Treat &nick as a keyword (in MUC) + + + + + + + Match whole &words + + + + + + + Match &case + + + + + + + + + + Actions + + + + + + &Highlight text + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 28 + 20 + + + + + + + + false + + + Custom c&olors: + + + + + + + false + + + &Foreground + + + Qt::ToolButtonTextBesideIcon + + + + + + + false + + + &Background + + + Qt::ToolButtonTextBesideIcon + + + + + + + + + &Play sound + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 28 + 20 + + + + + + + + false + + + Custom soun&d: + + + + + + + false + + + true + + + + + + + false + + + ... + + + + + + + + + + + + Qt::Vertical + + + + 20 + 101 + + + + + + + + + Swift::QtColorToolButton + QToolButton +
QtColorToolButton.h
+
+
+ + +
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 +#include +#include + +#include +#include + +#include +#include +#include + +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::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("") + (qs.isEmpty() ? "" : separator + qs); + } + return QVariant(qs); + } + case Action: { + std::vector 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::size_type>(index.row()) < highlightManager_->getRules().size()) { + HighlightRule r = highlightManager_->getRule(index.row()); + std::vector 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 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); // "" + 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(); + r.getAction().setTextColor(c.isValid() ? Q2PSTRING(c.name()) : ""); + break; + } + case TextBackground: { + QColor c = value.value(); + 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::size_type>(row1) < highlightManager_->getRules().size() && boost::numeric_cast::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 + +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 #include #include +#include #include #include #include @@ -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()); } +void QtLoginWindow::handleShowHighlightEditor() { + uiEventStream_->send(boost::make_shared()); +} + 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 #include "Whiteboard/QtWhiteboardWindow.h" #include #include @@ -162,6 +163,10 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(boost::shared_ptr 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); + virtual HighlightEditorWidget* createHighlightEditorWidget(); virtual void createAdHocCommandWindow(boost::shared_ptr command); private slots: diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 607a8a6..cd0ed57 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -113,6 +113,10 @@ sources = [ "QtContactEditWindow.cpp", "QtContactEditWidget.cpp", "QtSingleWindow.cpp", + "QtHighlightEditorWidget.cpp", + "QtHighlightRulesItemModel.cpp", + "QtHighlightRuleWidget.cpp", + "QtColorToolButton.cpp", "ChatSnippet.cpp", "MessageSnippet.cpp", "SystemMessageSnippet.cpp", @@ -231,6 +235,8 @@ myenv.Uic4("QtAffiliationEditor.ui") myenv.Uic4("QtJoinMUCWindow.ui") myenv.Uic4("QtHistoryWindow.ui") myenv.Uic4("QtConnectionSettings.ui") +myenv.Uic4("QtHighlightRuleWidget.ui") +myenv.Uic4("QtHighlightEditorWidget.ui") myenv.Qrc("DefaultTheme.qrc") myenv.Qrc("Swift.qrc") -- cgit v0.10.2-6-g49f6