diff options
Diffstat (limited to 'Swift')
54 files changed, 2320 insertions, 2712 deletions
diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index a498067..d1cd1fe 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -28,7 +28,7 @@ #include <Swift/Controllers/Chat/ChatMessageParser.h> #include <Swift/Controllers/FileTransfer/FileTransferController.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/StatusUtil.h> @@ -51,7 +51,7 @@ namespace Swift { * The controller does not gain ownership of the stanzaChannel, nor the factory. */ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) - : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, nickResolver, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { isInMUC_ = isInMUC; lastWasPresence_ = false; chatStateNotifier_ = new ChatStateNotifier(stanzaChannel, contact, entityCapsProvider); @@ -210,9 +210,10 @@ void ChatController::preHandleIncomingMessage(std::shared_ptr<MessageEvent> mess } void ChatController::postHandleIncomingMessage(std::shared_ptr<MessageEvent> messageEvent, const ChatWindow::ChatMessage& chatMessage) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index da9064e..5839d6c 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -26,8 +26,8 @@ #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> -#include <Swift/Controllers/HighlightManager.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> @@ -38,13 +38,13 @@ namespace Swift { -ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { +ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); chatWindow_->onLogCleared.connect(boost::bind(&ChatControllerBase::handleLogCleared, this)); entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1)); - highlighter_ = highlightManager->createHighlighter(); + highlighter_ = highlightManager->createHighlighter(nickResolver); ChatControllerBase::setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable()); createDayChangeTimer(); } @@ -204,30 +204,12 @@ bool ChatControllerBase::hasOpenWindow() const { return chatWindow_ && chatWindow_->isVisible(); } -ChatWindow::ChatMessage ChatControllerBase::buildChatWindowChatMessage(const std::string& message, bool senderIsSelf, const HighlightAction& fullMessageHighlightAction) { +ChatWindow::ChatMessage ChatControllerBase::buildChatWindowChatMessage(const std::string& message, const std::string& senderName, bool senderIsSelf) { ChatWindow::ChatMessage chatMessage; - chatMessage = chatMessageParser_->parseMessageBody(message, highlighter_->getNick(), senderIsSelf); - chatMessage.setFullMessageHighlightAction(fullMessageHighlightAction); + chatMessage = chatMessageParser_->parseMessageBody(message, senderName, senderIsSelf); return chatMessage; } -void ChatControllerBase::handleHighlightActions(const ChatWindow::ChatMessage& chatMessage) { - std::set<std::string> playedSounds; - if (chatMessage.getFullMessageHighlightAction().playSound()) { - highlighter_->handleHighlightAction(chatMessage.getFullMessageHighlightAction()); - playedSounds.insert(chatMessage.getFullMessageHighlightAction().getSoundFile()); - } - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : chatMessage.getParts()) { - std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightMessage = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part); - if (highlightMessage && highlightMessage->action.playSound()) { - if (playedSounds.find(highlightMessage->action.getSoundFile()) == playedSounds.end()) { - highlighter_->handleHighlightAction(highlightMessage->action); - playedSounds.insert(highlightMessage->action.getSoundFile()); - } - } - } -} - void ChatControllerBase::updateMessageCount() { chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); @@ -314,12 +296,6 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> mes } onActivity(body); - // Highlight - HighlightAction fullMessageHighlight; - if (!isIncomingMessageFromMe(message)) { - fullMessageHighlight = highlighter_->findFirstFullMessageMatchAction(body, senderHighlightNameFromMessage(from)); - } - std::shared_ptr<Replace> replace = message->getPayload<Replace>(); bool senderIsSelf = isIncomingMessageFromMe(message); if (replace) { @@ -327,12 +303,12 @@ void ChatControllerBase::handleIncomingMessage(std::shared_ptr<MessageEvent> mes std::map<JID, std::string>::iterator lastMessage; lastMessage = lastMessagesUIID_.find(from); if (lastMessage != lastMessagesUIID_.end()) { - chatMessage = buildChatWindowChatMessage(body, senderIsSelf, fullMessageHighlight); + chatMessage = buildChatWindowChatMessage(body, senderHighlightNameFromMessage(from), senderIsSelf); replaceMessage(chatMessage, lastMessagesUIID_[from], timeStamp); } } else { - chatMessage = buildChatWindowChatMessage(body, senderIsSelf, fullMessageHighlight); + chatMessage = buildChatWindowChatMessage(body, senderHighlightNameFromMessage(from), senderIsSelf); addMessageHandleIncomingMessage(from, chatMessage, senderIsSelf, label, timeStamp); } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 4255c19..7f118bd 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -28,24 +28,25 @@ #include <Swiften/Presence/PresenceOracle.h> #include <Swiften/Queries/IQRouter.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/HistoryController.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/XMPPEvents/MessageEvent.h> namespace Swift { - class IQRouter; - class StanzaChannel; - class ChatWindowFactory; + class AutoAcceptMUCInviteDecider; class AvatarManager; - class UIEventStream; - class EventController; + class ChatMessageParser; + class ChatWindowFactory; class EntityCapsProvider; + class EventController; class HighlightManager; class Highlighter; - class ChatMessageParser; - class AutoAcceptMUCInviteDecider; + class IQRouter; + class NickResolver; + class StanzaChannel; + class UIEventStream; class ChatControllerBase : public boost::signals2::trackable { public: @@ -73,7 +74,7 @@ namespace Swift { boost::signals2::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC; protected: - ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); + ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, std::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); /** * Pass the Message appended, and the stanza used to send it. @@ -96,8 +97,7 @@ namespace Swift { /** JID any iq for account should go to - bare except for PMs */ virtual JID getBaseJID(); virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) = 0; - ChatWindow::ChatMessage buildChatWindowChatMessage(const std::string& message, bool senderIsSelf, const HighlightAction& fullMessageHighlightAction); - void handleHighlightActions(const ChatWindow::ChatMessage& chatMessage); + ChatWindow::ChatMessage buildChatWindowChatMessage(const std::string& message, const std::string& senderName, bool senderIsSelf); void updateMessageCount(); private: diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp index ec7df6c..1a822a1 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.cpp +++ b/Swift/Controllers/Chat/ChatMessageParser.cpp @@ -1,16 +1,18 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <algorithm> #include <memory> #include <utility> #include <vector> #include <boost/algorithm/string.hpp> +#include <boost/regex.hpp> #include <Swiften/Base/Regex.h> #include <Swiften/Base/String.h> @@ -19,14 +21,14 @@ namespace Swift { - ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode) - : emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) { + ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, std::shared_ptr<HighlightConfiguration> highlightConfiguration, Mode mode) : emoticons_(emoticons), highlightConfiguration_(highlightConfiguration), mode_(mode) { } typedef std::pair<std::string, std::string> StringPair; - ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& nick, bool senderIsSelf) { + ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& senderNickname, bool senderIsSelf) { ChatWindow::ChatMessage parsedMessage; + std::string remaining = body; if (boost::starts_with(body, "/me ")) { remaining = String::getSplittedAtFirst(body, ' ').second; @@ -60,8 +62,12 @@ namespace Swift { parsedMessage = emoticonHighlight(parsedMessage); if (!senderIsSelf) { /* do not highlight our own messsages */ - /* do word-based color highlighting */ - parsedMessage = splitHighlight(parsedMessage, nick); + // Highlight keywords and own mentions. + parsedMessage = splitHighlight(parsedMessage); + + // Highlight full message events like, specific sender, general + // incoming group message, or general incoming direct message. + parsedMessage = fullMessageHighlight(parsedMessage, senderNickname); } return parsedMessage; @@ -89,7 +95,7 @@ namespace Swift { boost::regex emoticonRegex(regexString); ChatWindow::ChatMessage newMessage; - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : parsedMessage.getParts()) { + for (const auto& part : parsedMessage.getParts()) { std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { try { @@ -137,61 +143,122 @@ namespace Swift { } parsedMessage.setParts(newMessage.getParts()); } + return parsedMessage; } - ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message, const std::string& nick) { - ChatWindow::ChatMessage parsedMessage = message; - - for (size_t i = 0; i < highlightRules_->getSize(); ++i) { - const HighlightRule& rule = highlightRules_->getRule(i); - if (rule.getMatchMUC() && !mucMode_) { - continue; /* this rule only applies to MUC's, and this is a CHAT */ - } else if (rule.getMatchChat() && mucMode_) { - continue; /* this rule only applies to CHAT's, and this is a MUC */ - } else if (rule.getAction().getTextBackground().empty() && rule.getAction().getTextColor().empty()) { - continue; /* do not try to highlight text, if no highlight color is specified */ + ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message) { + auto keywordToRegEx = [](const std::string& keyword, bool matchCaseSensitive) { + std::string escaped = Regex::escape(keyword); + boost::regex::flag_type flags = boost::regex::normal; + if (!matchCaseSensitive) { + flags |= boost::regex::icase; } - const std::vector<boost::regex> keywordRegex = rule.getKeywordRegex(nick); - for (const boost::regex& regex : keywordRegex) { - ChatWindow::ChatMessage newMessage; - for (std::shared_ptr<ChatWindow::ChatMessagePart> part : parsedMessage.getParts()) { - std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; - if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { - try { - boost::match_results<std::string::const_iterator> match; - const std::string& text = textPart->text; - std::string::const_iterator start = text.begin(); - while (regex_search(start, text.end(), match, regex)) { - std::string::const_iterator matchStart = match[0].first; - std::string::const_iterator matchEnd = match[0].second; - if (start != matchStart) { - /* If we're skipping over plain text since the previous emoticon, record it as plain text */ - newMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); - } - std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = std::make_shared<ChatWindow::ChatHighlightingMessagePart>(); - highlightPart->text = match.str(); - highlightPart->action = rule.getAction(); - newMessage.append(highlightPart); - start = matchEnd; - } - if (start != text.end()) { - /* If there's plain text after the last emoticon, record it */ - newMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); + return boost::regex("\\b" + escaped + "\\b", flags); + }; + + auto highlightKeywordInChatMessage = [&](const ChatWindow::ChatMessage& message, const std::string& keyword, bool matchCaseSensitive, const HighlightAction& action) { + ChatWindow::ChatMessage resultMessage; + + for (const auto& part : message.getParts()) { + std::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; + if ((textPart = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { + try { + boost::match_results<std::string::const_iterator> match; + const std::string& text = textPart->text; + std::string::const_iterator start = text.begin(); + while (regex_search(start, text.end(), match, keywordToRegEx(keyword, matchCaseSensitive))) { + std::string::const_iterator matchStart = match[0].first; + std::string::const_iterator matchEnd = match[0].second; + if (start != matchStart) { + /* If we're skipping over plain text since the previous emoticon, record it as plain text */ + resultMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); } + std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = std::make_shared<ChatWindow::ChatHighlightingMessagePart>(); + highlightPart->text = match.str(); + highlightPart->action = action; + resultMessage.append(highlightPart); + start = matchEnd; } - catch (std::runtime_error) { - /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ - newMessage.append(part); + if (start != text.end()) { + /* If there's plain text after the last emoticon, record it */ + resultMessage.append(std::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); } - } else { - newMessage.append(part); } + catch (std::runtime_error) { + /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ + resultMessage.append(part); + } + } else { + resultMessage.append(part); } - parsedMessage.setParts(newMessage.getParts()); } + return resultMessage; + }; + + ChatWindow::ChatMessage parsedMessage = message; + + // detect mentions of own nickname + HighlightAction ownMentionKeywordAction = highlightConfiguration_->ownMentionAction; + ownMentionKeywordAction.setSoundFilePath(boost::optional<std::string>()); + ownMentionKeywordAction.setSystemNotificationEnabled(false); + if (!getNick().empty() && !highlightConfiguration_->ownMentionAction.isEmpty()) { + auto nicknameHighlightedMessage = highlightKeywordInChatMessage(parsedMessage, nick_, false, ownMentionKeywordAction); + auto highlightedParts = nicknameHighlightedMessage.getParts(); + auto ownNicknamePart = std::find_if(highlightedParts.begin(), highlightedParts.end(), [&](std::shared_ptr<ChatWindow::ChatMessagePart>& part){ + auto highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part); + if (highlightPart && highlightPart->text == nick_) { + return true; + } + return false; + }); + if (ownNicknamePart != highlightedParts.end()) { + parsedMessage.setHighlightActionOwnMention(highlightConfiguration_->ownMentionAction); + } + parsedMessage.setParts(nicknameHighlightedMessage.getParts()); + } + + // detect keywords + for (const auto& keywordHighlight : highlightConfiguration_->keywordHighlights) { + if (keywordHighlight.keyword.empty() || keywordHighlight.action.isEmpty()) { + continue; + } + auto newMessage = highlightKeywordInChatMessage(parsedMessage, keywordHighlight.keyword, keywordHighlight.matchCaseSensitive, keywordHighlight.action); + parsedMessage.setParts(newMessage.getParts()); } return parsedMessage; } + + ChatWindow::ChatMessage ChatMessageParser::fullMessageHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& sender) { + auto fullHighlightedMessage = parsedMessage; + + // contact highlighting + for (const auto& contactHighlight : highlightConfiguration_->contactHighlights) { + if (sender == contactHighlight.name) { + fullHighlightedMessage.setHighlightActionSender(contactHighlight.action); + break; + } + } + + // general incoming messages + HighlightAction groupAction; + HighlightAction chatAction; + + switch (mode_) { + case Mode::GroupChat: + groupAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingGroupchatMessages ? boost::optional<std::string>("") : boost::optional<std::string>()); + groupAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingGroupchatMessages); + fullHighlightedMessage.setHighlightActionGroupMessage(groupAction); + break; + + case Mode::Chat: + chatAction.setSoundFilePath(highlightConfiguration_->playSoundOnIncomingDirectMessages ? boost::optional<std::string>("") : boost::optional<std::string>()); + chatAction.setSystemNotificationEnabled(highlightConfiguration_->showNotificationOnIncomingDirectMessages); + fullHighlightedMessage.setHighlightActonDirectMessage(chatAction); + break; + } + + return fullHighlightedMessage; + } } diff --git a/Swift/Controllers/Chat/ChatMessageParser.h b/Swift/Controllers/Chat/ChatMessageParser.h index 4bed669..de5eac9 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.h +++ b/Swift/Controllers/Chat/ChatMessageParser.h @@ -1,26 +1,44 @@ /* - * Copyright (c) 2013-2014 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include <memory> #include <string> +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> namespace Swift { + /** + * @brief The ChatMessageParser class takes an emoticon map, a \ref HighlightConfiguration, and a boolean that indicates if the message context is in a MUC or not. + * The class handles parsing a message string and identifies emoticons, URLs, and various highlights. + */ class ChatMessageParser { public: - ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode = false); - ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& nick = "", bool senderIsSelf = false); + enum class Mode { Chat, GroupChat }; + + public: + ChatMessageParser(const std::map<std::string, std::string>& emoticons, std::shared_ptr<HighlightConfiguration> highlightConfiguration, Mode mode = Mode::Chat); + + void setNick(const std::string& nick) { nick_ = nick; } + std::string getNick() const { return nick_; } + + ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& sender = "", bool senderIsSelf = false); + private: ChatWindow::ChatMessage emoticonHighlight(const ChatWindow::ChatMessage& parsedMessage); - ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& nick); + ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage); + ChatWindow::ChatMessage fullMessageHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& sender); + + private: std::map<std::string, std::string> emoticons_; - HighlightRulesListPtr highlightRules_; - bool mucMode_; + std::shared_ptr<HighlightConfiguration> highlightConfiguration_; + Mode mode_; + std::string nick_; }; } diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index f55df1e..fc96701 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -746,7 +746,7 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); - std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), false); /* a message parser that knows this is a chat (not a room/MUC) */ + std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getConfiguration(), ChatMessageParser::Mode::Chat); /* a message parser that knows this is a chat (not a room/MUC) */ ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_); chatControllers_[contact] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); @@ -835,8 +835,8 @@ MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::opti if (reuseChatwindow) { chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); } - std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), true); /* a message parser that knows this is a room/MUC (not a chat) */ - controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_, mucBookmarkManager_); + std::shared_ptr<ChatMessageParser> chatMessageParser = std::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getConfiguration(), ChatMessageParser::Mode::GroupChat); /* a message parser that knows this is a room/MUC (not a chat) */ + controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, nickResolver_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_, mucBookmarkManager_); if (chatWindowFactoryAdapter) { /* The adapters are only passed to chat windows, which are deleted in their * controllers' dtor, which are deleted in ChatManager's dtor. The adapters diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index df54d73..c476cf3 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -34,7 +34,7 @@ #include <SwifTools/TabComplete.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> -#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> #include <Swift/Controllers/Roster/GroupRosterItem.h> @@ -80,6 +80,7 @@ MUCController::MUCController ( StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, + NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* uiEventStream, @@ -97,7 +98,7 @@ MUCController::MUCController ( AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager) : - ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { + ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), nickResolver, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false), clientBlockListManager_(clientBlockListManager), mucBookmarkManager_(mucBookmarkManager) { parting_ = true; joined_ = false; lastWasPresence_ = false; @@ -134,8 +135,7 @@ MUCController::MUCController ( muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2)); muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); - highlighter_->setMode(isImpromptu_ ? Highlighter::ChatMode : Highlighter::MUCMode); - highlighter_->setNick(nick_); + chatMessageParser_->setNick(nick_); if (timerFactory && stanzaChannel_->isAvailable()) { loginCheckTimer_ = std::shared_ptr<Timer>(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS)); loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this)); @@ -593,10 +593,11 @@ void MUCController::postHandleIncomingMessage(std::shared_ptr<MessageEvent> mess std::shared_ptr<Message> message = messageEvent->getStanza(); if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && !message->getPayload<Delay>()) { if (messageTargetsMe(message) || isImpromptu_) { + highlighter_->handleSystemNotifications(chatMessage, messageEvent); eventController_->handleIncomingEvent(messageEvent); } if (!messageEvent->getConcluded()) { - handleHighlightActions(chatMessage); + highlighter_->handleSoundNotifications(chatMessage); } } } @@ -1111,7 +1112,7 @@ void MUCController::checkDuplicates(std::shared_ptr<Message> newMessage) { void MUCController::setNick(const std::string& nick) { nick_ = nick; - highlighter_->setNick(nick_); + chatMessageParser_->setNick(nick); } Form::ref MUCController::buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm) { diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index 7ec2eb4..6244f6d 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -54,7 +54,7 @@ namespace Swift { class MUCController : public ChatControllerBase { public: - MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* xmppRoster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); + MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* xmppRoster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, std::shared_ptr<ChatMessageParser> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager, MUCBookmarkManager* mucBookmarkManager); virtual ~MUCController(); boost::signals2::signal<void ()> onUserLeft; boost::signals2::signal<void ()> onUserJoined; diff --git a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp index bc72b33..163a38a 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp @@ -1,30 +1,22 @@ /* - * Copyright (c) 2013-2016 Isode Limited. + * Copyright (c) 2013-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> -#include <hippomocks.h> +#include <gtest/gtest.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Settings/DummySettingsProvider.h> using namespace Swift; -class ChatMessageParserTest : public CppUnit::TestFixture { - CPPUNIT_TEST_SUITE(ChatMessageParserTest); - CPPUNIT_TEST(testFullBody); - CPPUNIT_TEST(testOneEmoticon); - CPPUNIT_TEST(testBareEmoticon); - CPPUNIT_TEST(testHiddenEmoticon); - CPPUNIT_TEST(testEndlineEmoticon); - CPPUNIT_TEST(testBoundedEmoticons); - CPPUNIT_TEST(testNoColourNoHighlight); - CPPUNIT_TEST_SUITE_END(); - -public: - void setUp() { +// Common test state +class ChatMessageParserTest : public ::testing::Test { +protected: + virtual void SetUp() { smile1_ = ":)"; smile1Path_ = "/blah/smile1.png"; smile2_ = ":("; @@ -33,244 +25,54 @@ public: emoticons_[smile2_] = smile2Path_; } - void tearDown() { + virtual void TearDown() { emoticons_.clear(); } - void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + static void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr<ChatWindow::ChatTextMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(result.getParts()[index]); - CPPUNIT_ASSERT_EQUAL(text, part->text); + ASSERT_EQ(text, part->text); } - void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { + static void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr<ChatWindow::ChatEmoticonMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(result.getParts()[index]); - CPPUNIT_ASSERT(!!part); - CPPUNIT_ASSERT_EQUAL(text, part->alternativeText); - CPPUNIT_ASSERT_EQUAL(path, part->imagePath); + ASSERT_NE(nullptr, part); + ASSERT_EQ(text, part->alternativeText); + ASSERT_EQ(path, part->imagePath); } -#define assertHighlight(RESULT, INDEX, TEXT, EXPECTED_HIGHLIGHT) \ - { \ - std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(RESULT.getParts()[INDEX]); \ - CPPUNIT_ASSERT_EQUAL(std::string(TEXT), part->text); \ - CPPUNIT_ASSERT(EXPECTED_HIGHLIGHT == part->action); \ - } - - void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + static void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + ASSERT_LT(index, result.getParts().size()); std::shared_ptr<ChatWindow::ChatURIMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(result.getParts()[index]); - CPPUNIT_ASSERT_EQUAL(text, part->target); - } - - static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) - { - HighlightRule rule; - std::vector<std::string> keywords; - keywords.push_back(keyword); - rule.setKeywords(keywords); - rule.setMatchCase(matchCase); - rule.setMatchWholeWords(matchWholeWord); - rule.setMatchChat(true); - rule.getAction().setTextBackground("white"); - return rule; - } - - static const HighlightRulesListPtr ruleListFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) - { - std::shared_ptr<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord)); - return list; - } - - static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2) - { - std::shared_ptr<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - list->addRule(rule1); - list->addRule(rule2); - return list; - } - - static HighlightRulesListPtr ruleListWithNickHighlight(bool withHighlightColour = true) - { - HighlightRule rule; - rule.setMatchChat(true); - rule.setNickIsKeyword(true); - rule.setMatchCase(true); - rule.setMatchWholeWords(true); - if (withHighlightColour) { - rule.getAction().setTextBackground("white"); - } - std::shared_ptr<HighlightManager::HighlightRulesList> list = std::make_shared<HighlightManager::HighlightRulesList>(); - list->addRule(rule); - return list; - } - - void testFullBody() { - const std::string no_special_message = "a message with no special content"; - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(no_special_message); - assertText(result, 0, no_special_message); - - HighlightRulesListPtr highlightRuleList = ruleListFromKeyword("trigger", false, false); - testling = ChatMessageParser(emoticons_, highlightRuleList); - result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); - assertEmoticon(result, 0, smile1_, smile1Path_); - assertText(result, 1, " shiny "); - assertEmoticon(result, 2, smile2_, smile2Path_); - assertText(result, 3, " "); - assertHighlight(result, 4, "trigger", highlightRuleList->getRule(0).getAction()); - assertText(result, 5, " "); - assertEmoticon(result, 6, smile1_, smile1Path_); - assertText(result, 7, " "); - assertURL(result, 8, "http://wonderland.lit/blah"); - assertText(result, 9, " "); - assertURL(result, 10, "http://denmark.lit"); - assertText(result, 11, " boom boom"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("testtriggermessage"); - assertText(result, 0, "test"); - assertHighlight(result, 1, "trigger", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "message"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, true)); - result = testling.parseMessageBody("testtriggermessage"); - assertText(result, 0, "testtriggermessage"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", true, false)); - result = testling.parseMessageBody("TrIgGeR"); - assertText(result, 0, "TrIgGeR"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("TrIgGeR"); - assertHighlight(result, 0, "TrIgGeR", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); - result = testling.parseMessageBody("partialTrIgGeRmatch"); - assertText(result, 0, "partial"); - assertHighlight(result, 1, "TrIgGeR", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "match"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zero one two three"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "one", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two "); - assertHighlight(result, 3, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "oNe", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two "); - assertHighlight(result, 3, "tHrEe", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", true, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero "); - assertHighlight(result, 1, "oNe", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " two tHrEe"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", true, false))); - result = testling.parseMessageBody("zero oNe two tHrEe"); - assertText(result, 0, "zero oNe two tHrEe"); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zero"); - assertHighlight(result, 1, "one", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, "two"); - assertHighlight(result, 3, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroOnEtwoThReE"); - assertText(result, 0, "zeroOnEtwo"); - assertHighlight(result, 1, "ThReE", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, false))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zeroonetwo"); - assertHighlight(result, 1, "three", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, true))); - result = testling.parseMessageBody("zeroonetwothree"); - assertText(result, 0, "zeroonetwothree"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Alice", "Alice"); - assertHighlight(result, 0, "Alice", highlightRuleList->getRule(0).getAction()); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("TextAliceText", "Alice"); - assertText(result, 0, "TextAliceText"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Text Alice Text", "Alice"); - assertText(result, 0, "Text "); - assertHighlight(result, 1, "Alice", highlightRuleList->getRule(0).getAction()); - assertText(result, 2, " Text"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Alice Text", "Alice"); - assertHighlight(result, 0, "Alice", highlightRuleList->getRule(0).getAction()); - assertText(result, 1, " Text"); - - testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); - result = testling.parseMessageBody("Text Alice", "Alice"); - assertText(result, 0, "Text "); - assertHighlight(result, 1, "Alice", highlightRuleList->getRule(0).getAction()); - } - - void testOneEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(" :) "); - assertText(result, 0, " "); - assertEmoticon(result, 1, smile1_, smile1Path_); - assertText(result, 2, " "); - } - - - void testBareEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(":)"); - assertEmoticon(result, 0, smile1_, smile1Path_); - } - - void testHiddenEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a"); - assertText(result, 0, "b:)a"); - } - - void testEndlineEmoticon() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody("Lazy:)"); - assertText(result, 0, "Lazy"); - assertEmoticon(result, 1, smile1_, smile1Path_); + ASSERT_EQ(text, part->target); } - void testBoundedEmoticons() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody(":)Lazy:("); - assertEmoticon(result, 0, smile1_, smile1Path_); - assertText(result, 1, "Lazy"); - assertEmoticon(result, 2, smile2_, smile2Path_); + void assertHighlight(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const HighlightAction& action) { + ASSERT_LT(index, result.getParts().size()); + std::shared_ptr<ChatWindow::ChatHighlightingMessagePart> part = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(result.getParts()[index]); + ASSERT_EQ(std::string(text), part->text); + ASSERT_EQ(action, part->action); } - void testEmoticonParenthesis() { - ChatMessageParser testling(emoticons_, std::make_shared<HighlightManager::HighlightRulesList>()); - ChatWindow::ChatMessage result = testling.parseMessageBody("(Like this :))"); - assertText(result, 0, "(Like this "); - assertEmoticon(result, 1, smile1_, smile1Path_); - assertText(result, 2, ")"); + static const std::shared_ptr<HighlightConfiguration> highlightConfigFromKeyword(const std::string& keyword, bool matchCase) { + std::shared_ptr<HighlightConfiguration> config = std::make_shared<HighlightConfiguration>(); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = keyword; + keywordHighlight.matchCaseSensitive = matchCase; + keywordHighlight.action.setFrontColor(std::string("#121212")); + config->keywordHighlights.push_back(keywordHighlight); + return config; } - void testNoColourNoHighlight() { - ChatMessageParser testling(emoticons_, ruleListWithNickHighlight(false)); - ChatWindow::ChatMessage result = testling.parseMessageBody("Alice", "Alice"); - assertText(result, 0, "Alice"); + static const std::shared_ptr<HighlightConfiguration> mergeHighlightConfig(const std::shared_ptr<HighlightConfiguration>& configA, const std::shared_ptr<HighlightConfiguration>& configB) { + std::shared_ptr<HighlightConfiguration> config = std::make_shared<HighlightConfiguration>(); + config->keywordHighlights.insert(config->keywordHighlights.end(), configA->keywordHighlights.begin(), configA->keywordHighlights.end()); + config->keywordHighlights.insert(config->keywordHighlights.end(), configB->keywordHighlights.begin(), configB->keywordHighlights.end()); + return config; } -private: std::map<std::string, std::string> emoticons_; std::string smile1_; std::string smile1Path_; @@ -278,4 +80,213 @@ private: std::string smile2Path_; }; -CPPUNIT_TEST_SUITE_REGISTRATION(ChatMessageParserTest); +TEST_F(ChatMessageParserTest, testNoHighlightingWithEmtpyConfiguration) { + const std::string no_special_message = "a message with no special content"; + ChatMessageParser testling(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody(no_special_message); + assertText(result, 0, no_special_message); +} + +TEST_F(ChatMessageParserTest, testSimpleHighlightAndEmojiAndUrlParsing) { + auto highlightConfig = highlightConfigFromKeyword("trigger", false); + auto testling = ChatMessageParser(emoticons_, highlightConfig); + auto result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, " shiny "); + assertEmoticon(result, 2, smile2_, smile2Path_); + assertText(result, 3, " "); + assertHighlight(result, 4, "trigger", highlightConfig->keywordHighlights[0].action); + assertText(result, 5, " "); + assertEmoticon(result, 6, smile1_, smile1Path_); + assertText(result, 7, " "); + assertURL(result, 8, "http://wonderland.lit/blah"); + assertText(result, 9, " "); + assertURL(result, 10, "http://denmark.lit"); + assertText(result, 11, " boom boom"); +} + +TEST_F(ChatMessageParserTest, testNoKeywordHighlightAsPartOfLongerWords) { + auto testling = ChatMessageParser(emoticons_, highlightConfigFromKeyword("trigger", false)); + auto result = testling.parseMessageBody("testtriggermessage"); + assertText(result, 0, "testtriggermessage"); +} + +TEST_F(ChatMessageParserTest, testCaseInsensitiveKeyordHighlight) { + auto config = highlightConfigFromKeyword("trigger", true); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("TrIgGeR"); + assertText(result, 0, "TrIgGeR"); + + testling = ChatMessageParser(emoticons_, highlightConfigFromKeyword("trigger", false)); + result = testling.parseMessageBody("TrIgGeR"); + assertHighlight(result, 0, "TrIgGeR", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", false)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero one two three"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "one", config->keywordHighlights[0].action); + assertText(result, 2, " two "); + assertHighlight(result, 3, "three", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleCaseInsensitiveKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", false)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe", config->keywordHighlights[0].action); + assertText(result, 2, " two "); + assertHighlight(result, 3, "tHrEe", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testMultipleCaseSensitiveKeywordHighlights) { + auto config = mergeHighlightConfig(highlightConfigFromKeyword("one", false), highlightConfigFromKeyword("three", true)); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe", config->keywordHighlights[0].action); + assertText(result, 2, " two tHrEe"); + + config = mergeHighlightConfig(highlightConfigFromKeyword("one", true), highlightConfigFromKeyword("three", false)); + testling = ChatMessageParser(emoticons_, config); + result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero oNe two "); + assertHighlight(result, 1, "tHrEe", config->keywordHighlights[0].action); +} + +TEST_F(ChatMessageParserTest, testOneEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody(" :) "); + assertText(result, 0, " "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, " "); +} + +TEST_F(ChatMessageParserTest, testBareEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody(":)"); + assertEmoticon(result, 0, smile1_, smile1Path_); +} + +TEST_F(ChatMessageParserTest, testHiddenEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody("b:)a"); + assertText(result, 0, "b:)a"); +} + +TEST_F(ChatMessageParserTest, testEndlineEmoticon) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody("Lazy:)"); + assertText(result, 0, "Lazy"); + assertEmoticon(result, 1, smile1_, smile1Path_); +} + +TEST_F(ChatMessageParserTest, testBoundedEmoticons) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody(":)Lazy:("); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, "Lazy"); + assertEmoticon(result, 2, smile2_, smile2Path_); +} + +TEST_F(ChatMessageParserTest, testEmoticonParenthesis) { + auto testling = ChatMessageParser(emoticons_, std::make_shared<HighlightConfiguration>()); + auto result = testling.parseMessageBody("(Like this :))"); + assertText(result, 0, "(Like this "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, ")"); +} + +TEST_F(ChatMessageParserTest, testSenderAndKeywordHighlighting) { + auto config = std::make_shared<HighlightConfiguration>(); + auto contactHighlight = HighlightConfiguration::ContactHighlight(); + contactHighlight.action.setFrontColor(std::string("#f0f0f0")); + contactHighlight.action.setBackColor(std::string("#0f0f0f")); + contactHighlight.name = "Romeo"; + config->contactHighlights.push_back(contactHighlight); + auto keywordHighlight = HighlightConfiguration::KeywordHightlight(); + keywordHighlight.action.setFrontColor(std::string("#abcdef")); + keywordHighlight.action.setBackColor(std::string("#fedcba")); + keywordHighlight.keyword = "XMPP"; + config->keywordHighlights.push_back(keywordHighlight); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("Heard any news about xmpp recently?", "Romeo"); + assertText(result, 0, "Heard any news about "); + assertHighlight(result, 1, "xmpp", keywordHighlight.action); + assertText(result, 2, " recently?"); + ASSERT_EQ(contactHighlight.action, result.getHighlightActionSender()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testKeywordWithEmptyActionIsIgnored) { + auto config = std::make_shared<HighlightConfiguration>(); + auto keywordHighlight = HighlightConfiguration::KeywordHightlight(); + keywordHighlight.keyword = "XMPP"; + config->keywordHighlights.push_back(keywordHighlight); + auto testling = ChatMessageParser(emoticons_, config); + auto result = testling.parseMessageBody("Heard any news about xmpp recently?", "Romeo"); + assertText(result, 0, "Heard any news about xmpp recently?"); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testMeMessageAndOwnMention) { + auto config = std::make_shared<HighlightConfiguration>(); + config->ownMentionAction.setFrontColor(std::string("#f0f0f0")); + config->ownMentionAction.setBackColor(std::string("#0f0f0f")); + config->ownMentionAction.setSoundFilePath(std::string("someSoundFile.wav")); + auto ownMentionActionForPart = config->ownMentionAction; + ownMentionActionForPart.setSoundFilePath(boost::optional<std::string>()); + auto testling = ChatMessageParser(emoticons_, config); + testling.setNick("Juliet"); + auto result = testling.parseMessageBody("/me wonders when Juliet is coming?", "Romeo"); + assertText(result, 0, "wonders when "); + assertHighlight(result, 1, "Juliet", ownMentionActionForPart); + assertText(result, 2, " is coming?"); + ASSERT_EQ(true, result.isMeCommand()); + ASSERT_EQ(config->ownMentionAction, result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testSoundAndNotificationOnDirectMessage) { + auto defaultConfiguration = std::make_shared<HighlightConfiguration>(); + defaultConfiguration->playSoundOnIncomingDirectMessages = true; + defaultConfiguration->showNotificationOnIncomingDirectMessages = true; + defaultConfiguration->ownMentionAction.setFrontColor(std::string("black")); + defaultConfiguration->ownMentionAction.setBackColor(std::string("yellow")); + defaultConfiguration->ownMentionAction.setSoundFilePath(std::string("")); + defaultConfiguration->ownMentionAction.setSystemNotificationEnabled(true); + + auto testling = ChatMessageParser(emoticons_, defaultConfiguration, ChatMessageParser::Mode::Chat); + auto result = testling.parseMessageBody("I wonder when Juliet is coming?", "Romeo"); + + ASSERT_EQ(std::string(""), result.getHighlightActionDirectMessage().getSoundFilePath().get_value_or(std::string("somethingElse"))); + ASSERT_EQ(true, result.getHighlightActionDirectMessage().isSystemNotificationEnabled()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); +} + +TEST_F(ChatMessageParserTest, testWithDefaultConfiguration) { + DummySettingsProvider settings; + HighlightManager manager(&settings); + manager.resetToDefaultConfiguration(); + auto testling = ChatMessageParser(emoticons_, manager.getConfiguration(), ChatMessageParser::Mode::GroupChat); + testling.setNick("Juliet"); + auto result = testling.parseMessageBody("Hello, how is it going?", "Romeo"); + assertText(result, 0, "Hello, how is it going?"); + ASSERT_EQ(HighlightAction(), result.getHighlightActionOwnMention()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionDirectMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionGroupMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionSender()); + + result = testling.parseMessageBody("Juliet, seen the new interface design?", "Romeo"); + auto mentionKeywordAction = manager.getConfiguration()->ownMentionAction; + mentionKeywordAction.setSoundFilePath(boost::optional<std::string>()); + mentionKeywordAction.setSystemNotificationEnabled(false); + assertHighlight(result, 0, "Juliet", mentionKeywordAction); + assertText(result, 1, ", seen the new interface design?"); + ASSERT_EQ(manager.getConfiguration()->ownMentionAction, result.getHighlightActionOwnMention()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionDirectMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionGroupMessage()); + ASSERT_EQ(HighlightAction(), result.getHighlightActionSender()); +} diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index cff54f8..80f8346 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -131,7 +131,7 @@ public: wbSessionManager_ = new WhiteboardSessionManager(iqRouter_, stanzaChannel_, presenceOracle_, entityCapsProvider_); wbManager_ = new WhiteboardManager(whiteboardWindowFactory_, uiEventStream_, nickResolver_, wbSessionManager_); highlightManager_ = new HighlightManager(settings_); - highlightManager_->resetToDefaultRulesList(); + highlightManager_->resetToDefaultConfiguration(); handledHighlightActions_ = 0; soundsPlayed_.clear(); highlightManager_->onHighlight.connect(boost::bind(&ChatsManagerTest::handleHighlightAction, this, _1)); @@ -786,24 +786,17 @@ public: } void testChatControllerHighlightingNotificationTesting() { - HighlightRule keywordRuleA; - keywordRuleA.setMatchChat(true); - std::vector<std::string> keywordsA; - keywordsA.push_back("Romeo"); - keywordRuleA.setKeywords(keywordsA); - keywordRuleA.getAction().setTextColor("yellow"); - keywordRuleA.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleA); - - HighlightRule keywordRuleB; - keywordRuleB.setMatchChat(true); - std::vector<std::string> keywordsB; - keywordsB.push_back("Juliet"); - keywordRuleB.setKeywords(keywordsB); - keywordRuleB.getAction().setTextColor("green"); - keywordRuleB.getAction().setPlaySound(true); - keywordRuleB.getAction().setSoundFile("/tmp/someotherfile.wav"); - highlightManager_->insertRule(0, keywordRuleB); + HighlightConfiguration::KeywordHightlight keywordRuleA; + keywordRuleA.keyword = "Romeo"; + keywordRuleA.action.setFrontColor(boost::optional<std::string>("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + HighlightConfiguration::KeywordHightlight keywordRuleB; + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional<std::string>("green")); + keywordRuleB.action.setSoundFilePath(boost::optional<std::string>("/tmp/someotherfile.wav")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); @@ -817,28 +810,22 @@ public: manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(2, handledHighlightActions_); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.getAction().getSoundFile()) != soundsPlayed_.end()); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.getAction().getSoundFile()) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); } void testChatControllerHighlightingNotificationDeduplicateSounds() { - HighlightRule keywordRuleA; - keywordRuleA.setMatchChat(true); - std::vector<std::string> keywordsA; - keywordsA.push_back("Romeo"); - keywordRuleA.setKeywords(keywordsA); - keywordRuleA.getAction().setTextColor("yellow"); - keywordRuleA.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleA); - - HighlightRule keywordRuleB; - keywordRuleB.setMatchChat(true); - std::vector<std::string> keywordsB; - keywordsB.push_back("Juliet"); - keywordRuleB.setKeywords(keywordsB); - keywordRuleB.getAction().setTextColor("green"); - keywordRuleB.getAction().setPlaySound(true); - highlightManager_->insertRule(0, keywordRuleB); + auto keywordRuleA = HighlightConfiguration::KeywordHightlight(); + keywordRuleA.keyword = "Romeo"; + keywordRuleA.action.setFrontColor(boost::optional<std::string>("yellow")); + keywordRuleA.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleA); + + auto keywordRuleB = HighlightConfiguration::KeywordHightlight(); + keywordRuleB.keyword = "Juliet"; + keywordRuleB.action.setFrontColor(boost::optional<std::string>("green")); + keywordRuleB.action.setSoundFilePath(boost::optional<std::string>("")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordRuleB); JID messageJID = JID("testling@test.com"); @@ -852,8 +839,8 @@ public: manager_->handleIncomingMessage(message); CPPUNIT_ASSERT_EQUAL(1, handledHighlightActions_); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.getAction().getSoundFile()) != soundsPlayed_.end()); - CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.getAction().getSoundFile()) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleA.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); + CPPUNIT_ASSERT(soundsPlayed_.find(keywordRuleB.action.getSoundFilePath().get_value_or("")) != soundsPlayed_.end()); } void testChatControllerMeMessageHandling() { @@ -922,12 +909,13 @@ public: JID mucJID("mucroom@rooms.test.com"); std::string nickname = "toodles"; + //highlightManager_->resetToDefaultConfiguration(); + // add highlight rule for 'foo' - HighlightRule fooHighlight; - fooHighlight.setKeywords({"foo"}); - fooHighlight.setMatchMUC(true); - fooHighlight.getAction().setTextBackground("green"); - highlightManager_->insertRule(0, fooHighlight); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = "foo"; + keywordHighlight.action.setBackColor(boost::optional<std::string>("green")); + highlightManager_->getConfiguration()->keywordHighlights.push_back(keywordHighlight); MockChatWindow* window = new MockChatWindow(); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(mucJID, uiEventStream_).Return(window); @@ -1141,8 +1129,8 @@ private: void handleHighlightAction(const HighlightAction& action) { handledHighlightActions_++; - if (action.playSound()) { - soundsPlayed_.insert(action.getSoundFile()); + if (action.getSoundFilePath()) { + soundsPlayed_.insert(action.getSoundFilePath().get_value_or("")); } } diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index dad021f..eabf4c5 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -94,18 +94,20 @@ public: highlightManager_ = new HighlightManager(settings_); muc_ = std::make_shared<MockMUC>(mucJID_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); - chatMessageParser_ = std::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getRules(), true); + chatMessageParser_ = std::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getConfiguration(), ChatMessageParser::Mode::GroupChat); vcardStorage_ = new VCardMemoryStorage(crypto_.get()); vcardManager_ = new VCardManager(self_, iqRouter_, vcardStorage_); + nickResolver_ = new NickResolver(self_, xmppRoster_, vcardManager_, mucRegistry_); clientBlockListManager_ = new ClientBlockListManager(iqRouter_); mucBookmarkManager_ = new MUCBookmarkManager(iqRouter_); - controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, nullptr, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, false, nullptr, vcardManager_, mucBookmarkManager_); + controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, nickResolver_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, nullptr, nullptr, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser_, false, nullptr, vcardManager_, mucBookmarkManager_); } void tearDown() { delete controller_; delete mucBookmarkManager_; delete clientBlockListManager_; + delete nickResolver_; delete vcardManager_; delete vcardStorage_; delete highlightManager_; @@ -592,7 +594,7 @@ private: ChatWindowFactory* chatWindowFactory_; UserSearchWindowFactory* userSearchWindowFactory_; MUCController* controller_; -// NickResolver* nickResolver_; + NickResolver* nickResolver_; PresenceOracle* presenceOracle_; AvatarManager* avatarManager_; StanzaChannelPresenceSender* presenceSender_; diff --git a/Swift/Controllers/EventNotifier.cpp b/Swift/Controllers/EventNotifier.cpp index 6ea2ea5..f22a58c 100644 --- a/Swift/Controllers/EventNotifier.cpp +++ b/Swift/Controllers/EventNotifier.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -42,17 +42,14 @@ void EventNotifier::handleEventAdded(std::shared_ptr<StanzaEvent> event) { } if (std::shared_ptr<MessageEvent> messageEvent = std::dynamic_pointer_cast<MessageEvent>(event)) { JID jid = messageEvent->getStanza()->getFrom(); - std::string title = nickResolver->jidToNick(jid); if (!messageEvent->getStanza()->isError() && !messageEvent->getStanza()->getBody().get_value_or("").empty()) { JID activationJID = jid; if (messageEvent->getStanza()->getType() == Message::Groupchat) { activationJID = jid.toBare(); } - std::string messageText = messageEvent->getStanza()->getBody().get_value_or(""); - if (boost::starts_with(messageText, "/me ")) { - messageText = "*" + String::getSplittedAtFirst(messageText, ' ').second + "*"; + for (const auto& notification : messageEvent->getNotifications()) { + notifier->showMessage(Notifier::IncomingMessage, notification.title, notification.message, avatarManager->getAvatarPath(jid), boost::bind(&EventNotifier::handleNotificationActivated, this, activationJID)); } - notifier->showMessage(Notifier::IncomingMessage, title, messageText, avatarManager->getAvatarPath(jid), boost::bind(&EventNotifier::handleNotificationActivated, this, activationJID)); } } else if(std::shared_ptr<SubscriptionRequestEvent> subscriptionEvent = std::dynamic_pointer_cast<SubscriptionRequestEvent>(event)) { diff --git a/Swift/Controllers/HighlightAction.cpp b/Swift/Controllers/HighlightAction.cpp deleted file mode 100644 index 3ea2c86..0000000 --- a/Swift/Controllers/HighlightAction.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2015 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/Controllers/HighlightAction.h> - -namespace Swift { - -void HighlightAction::setHighlightWholeMessage(bool highlightText) -{ - highlightWholeMessage_ = highlightText; - if (!highlightWholeMessage_) { - textColor_.clear(); - textBackground_.clear(); - } -} - -void HighlightAction::setPlaySound(bool playSound) -{ - playSound_ = playSound; - if (!playSound_) { - soundFile_.clear(); - } -} - -bool operator ==(HighlightAction const& a, HighlightAction const& b) { - if (a.highlightWholeMessage() != b.highlightWholeMessage()) { - return false; - } - - if (a.getTextColor() != b.getTextColor()) { - return false; - } - - if (a.getTextBackground() != b.getTextBackground()) { - return false; - } - - if (a.playSound() != b.playSound()) { - return false; - } - - if (a.getSoundFile() != b.getSoundFile()) { - return false; - } - - return true; -} - -bool operator !=(HighlightAction const& a, HighlightAction const& b) { - return !(a == b); -} - -} diff --git a/Swift/Controllers/HighlightAction.h b/Swift/Controllers/HighlightAction.h deleted file mode 100644 index b9d4539..0000000 --- a/Swift/Controllers/HighlightAction.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2015 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <string> - -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> - -namespace Swift { - - class HighlightRule; - - class HighlightAction { - public: - HighlightAction() : highlightWholeMessage_(false), playSound_(false) {} - - /** - * Gets the flag that indicates the entire message should be highlighted. - */ - bool highlightWholeMessage() const { return highlightWholeMessage_; } - void setHighlightWholeMessage(bool highlightText); - - /** - * Gets the foreground highlight color. - */ - const std::string& getTextColor() const { return textColor_; } - void setTextColor(const std::string& textColor) { textColor_ = textColor; } - - /** - * Gets the background highlight color. - */ - const std::string& getTextBackground() const { return textBackground_; } - void setTextBackground(const std::string& textBackground) { textBackground_ = textBackground; } - - bool playSound() const { return playSound_; } - void setPlaySound(bool playSound); - - /** - * Gets the sound filename. If the string is empty, assume a default sound file. - */ - const std::string& getSoundFile() const { return soundFile_; } - void setSoundFile(const std::string& soundFile) { soundFile_ = soundFile; } - - bool isEmpty() const { return !highlightWholeMessage_ && !playSound_; } - - private: - friend class boost::serialization::access; - template<class Archive> void serialize(Archive & ar, const unsigned int version); - - bool highlightWholeMessage_; - std::string textColor_; - std::string textBackground_; - - bool playSound_; - std::string soundFile_; - }; - - bool operator ==(HighlightAction const& a, HighlightAction const& b); - bool operator !=(HighlightAction const& a, HighlightAction const& b); - - template<class Archive> - void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) - { - ar & highlightWholeMessage_; - ar & textColor_; - ar & textBackground_; - ar & playSound_; - ar & soundFile_; - } - -} diff --git a/Swift/Controllers/HighlightManager.cpp b/Swift/Controllers/HighlightManager.cpp deleted file mode 100644 index 9176301..0000000 --- a/Swift/Controllers/HighlightManager.cpp +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/Controllers/HighlightManager.h> - -#include <cassert> -#include <sstream> - -#include <boost/algorithm/string.hpp> -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> -#include <boost/bind.hpp> -#include <boost/numeric/conversion/cast.hpp> -#include <boost/regex.hpp> -#include <boost/serialization/vector.hpp> - -#include <Swift/Controllers/Highlighter.h> -#include <Swift/Controllers/SettingConstants.h> -#include <Swift/Controllers/Settings/SettingsProvider.h> - -/* How does highlighting work? - * - * HighlightManager manages a list of if-then rules used to highlight messages. - * Rule is represented by HighlightRule. Action ("then" part) is HighlightAction. - * - * - * HighlightManager is also used as a factory for Highlighter objects. - * Each ChatControllerBase has its own Highlighter. - * Highligher may be customized by using setNick(), etc. - * - * ChatControllerBase passes incoming messages to Highlighter and gets HighlightAction in return - * (first matching rule is returned). - * If needed, HighlightAction is then passed back to Highlighter for further handling. - * This results in HighlightManager emiting onHighlight event, - * which is handled by SoundController to play sound notification - */ - -namespace Swift { - -HighlightManager::HighlightManager(SettingsProvider* settings) - : settings_(settings) - , storingSettings_(false) { - rules_ = std::make_shared<HighlightRulesList>(); - loadSettings(); - handleSettingChangedConnection_ = settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1)); -} - -void HighlightManager::handleSettingChanged(const std::string& settingPath) { - if (!storingSettings_ && SettingConstants::HIGHLIGHT_RULES.getKey() == settingPath) { - loadSettings(); - } -} - -std::string HighlightManager::rulesToString() const { - std::stringstream stream; - boost::archive::text_oarchive archive(stream); - archive & rules_->list_; - return stream.str(); -} - -std::vector<HighlightRule> HighlightManager::getDefaultRules() { - std::vector<HighlightRule> rules; - - HighlightRule chatNotificationRule; - chatNotificationRule.setMatchChat(true); - chatNotificationRule.getAction().setPlaySound(true); - chatNotificationRule.setMatchWholeWords(true); - rules.push_back(chatNotificationRule); - - HighlightRule selfMentionMUCRule; - selfMentionMUCRule.setMatchMUC(true); - selfMentionMUCRule.getAction().setPlaySound(true); - selfMentionMUCRule.setNickIsKeyword(true); - selfMentionMUCRule.setMatchCase(true); - selfMentionMUCRule.setMatchWholeWords(true); - rules.push_back(selfMentionMUCRule); - - return rules; -} - -HighlightRule HighlightManager::getRule(int index) const { - assert(index >= 0 && static_cast<size_t>(index) < rules_->getSize()); - return rules_->getRule(static_cast<size_t>(index)); -} - -void HighlightManager::setRule(int index, const HighlightRule& rule) { - assert(index >= 0 && static_cast<size_t>(index) < rules_->getSize()); - rules_->list_[static_cast<size_t>(index)] = rule; -} - -void HighlightManager::insertRule(int index, const HighlightRule& rule) { - assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) <= rules_->getSize()); - rules_->list_.insert(rules_->list_.begin() + index, rule); -} - -void HighlightManager::removeRule(int index) { - assert(index >= 0 && boost::numeric_cast<std::vector<std::string>::size_type>(index) < rules_->getSize()); - rules_->list_.erase(rules_->list_.begin() + index); -} - -void HighlightManager::swapRules(const size_t first, const size_t second) { - assert(first < rules_->getSize()); - assert(second < rules_->getSize()); - const HighlightRule swap = rules_->getRule(first); - rules_->setRule(first, rules_->getRule(second)); - rules_->setRule(second, swap); -} - -void HighlightManager::storeSettings() { - storingSettings_ = true; // don't reload settings while saving - settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES, rulesToString()); - storingSettings_ = false; -} - -void HighlightManager::loadSettings() { - std::string rulesString = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES); - std::stringstream stream; - stream << rulesString; - try { - boost::archive::text_iarchive archive(stream); - archive >> rules_->list_; - } catch (boost::archive::archive_exception&) { - rules_->list_ = getDefaultRules(); - } -} - -Highlighter* HighlightManager::createHighlighter() { - return new Highlighter(this); -} - -bool HighlightManager::isDefaultRulesList() const { - return getDefaultRules() == rules_->list_; -} - -void HighlightManager::resetToDefaultRulesList() { - rules_->list_ = getDefaultRules(); -} - -} diff --git a/Swift/Controllers/HighlightManager.h b/Swift/Controllers/HighlightManager.h deleted file mode 100644 index a35e253..0000000 --- a/Swift/Controllers/HighlightManager.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <string> -#include <vector> - -#include <boost/signals2.hpp> - -#include <Swift/Controllers/HighlightRule.h> - -namespace Swift { - - class SettingsProvider; - class Highlighter; - - class HighlightManager { - public: - - class HighlightRulesList { - public: - friend class HighlightManager; - size_t getSize() const { return list_.size(); } - const HighlightRule& getRule(const size_t index) const { return list_[index]; } - void addRule(const HighlightRule& rule) { list_.push_back(rule); } - void combineRules(const HighlightRulesList& rhs) { - list_.insert(list_.end(), rhs.list_.begin(), rhs.list_.end()); - } - void setRule(const size_t index, const HighlightRule& rule) { - list_[index] = rule; - } - private: - std::vector<HighlightRule> list_; - }; - - HighlightManager(SettingsProvider* settings); - - Highlighter* createHighlighter(); - - std::shared_ptr<const HighlightManager::HighlightRulesList> getRules() const { return rules_; } - - bool isDefaultRulesList() const; - void resetToDefaultRulesList(); - - HighlightRule getRule(int index) const; - void setRule(int index, const HighlightRule& rule); - void insertRule(int index, const HighlightRule& rule); - void removeRule(int index); - void swapRules(const size_t first, const size_t second); - void storeSettings(); - void loadSettings(); - - boost::signals2::signal<void (const HighlightAction&)> onHighlight; - - private: - void handleSettingChanged(const std::string& settingPath); - - std::string rulesToString() const; - static std::vector<HighlightRule> getDefaultRules(); - - private: - SettingsProvider* settings_; - bool storingSettings_; - - std::shared_ptr<HighlightManager::HighlightRulesList> rules_; - boost::signals2::scoped_connection handleSettingChangedConnection_; - }; - - typedef std::shared_ptr<const HighlightManager::HighlightRulesList> HighlightRulesListPtr; - -} diff --git a/Swift/Controllers/HighlightRule.cpp b/Swift/Controllers/HighlightRule.cpp deleted file mode 100644 index a8cb7e4..0000000 --- a/Swift/Controllers/HighlightRule.cpp +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/Controllers/HighlightRule.h> - -#include <algorithm> - -#include <boost/algorithm/string.hpp> -#include <boost/lambda/lambda.hpp> - -#include <Swiften/Base/Regex.h> - -namespace Swift { - -HighlightRule::HighlightRule() - : nickIsKeyword_(false) - , matchCase_(false) - , matchWholeWords_(false) - , matchChat_(false) - , matchMUC_(false) -{ -} - -boost::regex HighlightRule::regexFromString(const std::string & s) const -{ - std::string escaped = Regex::escape(s); - 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(); - for (const auto& k : keywords_) { - keywordRegex_.push_back(regexFromString(k)); - } - senderRegex_.clear(); - for (const auto& 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"; -} - -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(); - - if (!matchesKeyword) { - // check if the nickname matches - if (nickIsKeyword_ && !nick.empty() && boost::regex_search(body, regexFromString(nick))) { - matchesKeyword = true; - } - - // check if a keyword matches - if (!matchesKeyword && !keywords_.empty()) { - for (const auto& keyword : keywordRegex_) { - if (boost::regex_search(body, keyword)) { - matchesKeyword = true; - break; - } - } - } - } - - for (const auto& rx : senderRegex_) { - if (boost::regex_search(sender, rx)) { - matchesSender = true; - break; - } - } - - if (matchesKeyword && matchesSender) { - return true; - } - } - - return false; -} - -void HighlightRule::setSenders(const std::vector<std::string>& senders) -{ - senders_ = senders; - updateRegex(); -} - -void HighlightRule::setKeywords(const std::vector<std::string>& keywords) -{ - keywords_ = keywords; - updateRegex(); -} - -std::vector<boost::regex> HighlightRule::getKeywordRegex(const std::string& nick) const { - if (nickIsKeyword_) { - std::vector<boost::regex> regex; - if (!nick.empty()) { - regex.push_back(regexFromString(nick)); - } - return regex; - } else { - return keywordRegex_; - } -} - -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(); -} - -bool operator ==(HighlightRule const& a, HighlightRule const& b) { - if (a.getSenders() != b.getSenders()) { - return false; - } - - if (a.getKeywords() != b.getKeywords()) { - return false; - } - - if (a.getNickIsKeyword() != b.getNickIsKeyword()) { - return false; - } - - if (a.getMatchChat() != b.getMatchChat()) { - return false; - } - - if (a.getMatchMUC() != b.getMatchMUC()) { - return false; - } - - if (a.getMatchCase() != b.getMatchCase()) { - return false; - } - - if (a.getMatchWholeWords() != b.getMatchWholeWords()) { - return false; - } - - if (a.getAction() != b.getAction()) { - return false; - } - - return true; -} - -} diff --git a/Swift/Controllers/HighlightRule.h b/Swift/Controllers/HighlightRule.h deleted file mode 100644 index bffdc41..0000000 --- a/Swift/Controllers/HighlightRule.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <string> -#include <vector> - -#include <boost/archive/text_iarchive.hpp> -#include <boost/archive/text_oarchive.hpp> -#include <boost/regex.hpp> - -#include <Swift/Controllers/HighlightAction.h> - -namespace Swift { - - class HighlightRule { - public: - HighlightRule(); - - enum MessageType { ChatMessage, MUCMessage }; - - bool isMatch(const std::string& body, const std::string& sender, const std::string& nick, MessageType) const; - - const HighlightAction& getAction() const { return action_; } - HighlightAction& getAction() { return action_; } - - const std::vector<std::string>& getSenders() const { return senders_; } - void setSenders(const std::vector<std::string>&); - const std::vector<boost::regex>& getSenderRegex() const { return senderRegex_; } - - const std::vector<std::string>& getKeywords() const { return keywords_; } - void setKeywords(const std::vector<std::string>&); - std::vector<boost::regex> getKeywordRegex(const std::string& nick) const; - - 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: - friend class boost::serialization::access; - template<class Archive> void serialize(Archive & ar, const unsigned int version); - - static std::string boolToString(bool); - static bool boolFromString(const std::string&); - - std::vector<std::string> senders_; - std::vector<std::string> keywords_; - bool nickIsKeyword_; - - mutable std::vector<boost::regex> senderRegex_; - mutable std::vector<boost::regex> keywordRegex_; - void updateRegex() const; - boost::regex regexFromString(const std::string&) const; - - bool matchCase_; - bool matchWholeWords_; - - bool matchChat_; - bool matchMUC_; - - HighlightAction action_; - }; - - bool operator ==(HighlightRule const& a, HighlightRule const& b); - - template<class Archive> - void HighlightRule::serialize(Archive& ar, const unsigned int /*version*/) - { - ar & senders_; - ar & keywords_; - ar & nickIsKeyword_; - ar & matchChat_; - ar & matchMUC_; - ar & matchCase_; - ar & matchWholeWords_; - ar & action_; - updateRegex(); - } - -} diff --git a/Swift/Controllers/Highlighter.cpp b/Swift/Controllers/Highlighter.cpp deleted file mode 100644 index cea077e..0000000 --- a/Swift/Controllers/Highlighter.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/Controllers/Highlighter.h> - -#include <Swift/Controllers/HighlightManager.h> - -namespace Swift { - -Highlighter::Highlighter(HighlightManager* manager) - : manager_(manager) -{ - setMode(ChatMode); -} - -void Highlighter::setMode(Mode mode) -{ - mode_ = mode; - messageType_ = mode_ == ChatMode ? HighlightRule::ChatMessage : HighlightRule::MUCMessage; -} - -HighlightAction Highlighter::findFirstFullMessageMatchAction(const std::string& body, const std::string& sender) const -{ - HighlightAction match; - HighlightRulesListPtr rules = manager_->getRules(); - for (size_t i = 0; i < rules->getSize(); ++i) { - const HighlightRule& rule = rules->getRule(i); - if (rule.isMatch(body, sender, nick_, messageType_) && rule.getAction().highlightWholeMessage()) { - match = rule.getAction(); - break; - } - } - - return match; -} - -void Highlighter::handleHighlightAction(const HighlightAction& action) -{ - manager_->onHighlight(action); -} - -} diff --git a/Swift/Controllers/Highlighter.h b/Swift/Controllers/Highlighter.h deleted file mode 100644 index 9ad3339..0000000 --- a/Swift/Controllers/Highlighter.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <string> -#include <vector> - -#include <Swift/Controllers/HighlightRule.h> - -namespace Swift { - - class HighlightManager; - - class Highlighter { - public: - Highlighter(HighlightManager* manager); - - enum Mode { ChatMode, MUCMode }; - void setMode(Mode mode); - - void setNick(const std::string& nick) { nick_ = nick; } - std::string getNick() const { return nick_; } - - HighlightAction findFirstFullMessageMatchAction(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/Highlighting/HighlightAction.cpp b/Swift/Controllers/Highlighting/HighlightAction.cpp new file mode 100644 index 0000000..e9f14df --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightAction.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2015-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/Highlighting/HighlightAction.h> + +namespace Swift { + + + +bool operator ==(const HighlightAction& a, const HighlightAction& b) { + if (a.getFrontColor() != b.getFrontColor()) { + return false; + } + if (a.getBackColor() != b.getBackColor()) { + return false; + } + if (a.getSoundFilePath() != b.getSoundFilePath()) { + return false; + } + if (a.isSystemNotificationEnabled() != b.isSystemNotificationEnabled()) { + return false; + } + return true; +} + +bool operator !=(const HighlightAction& a, const HighlightAction& b) { + return !(a == b); +} + +void HighlightAction::setFrontColor(const boost::optional<std::string>& frontColor) { + frontColor_ = frontColor; +} + +boost::optional<std::string> HighlightAction::getFrontColor() const { + return frontColor_; +} + +void HighlightAction::setBackColor(const boost::optional<std::string>& backColor) { + backColor_ = backColor; +} + +boost::optional<std::string> HighlightAction::getBackColor() const { + return backColor_; +} + +void HighlightAction::setSoundFilePath(const boost::optional<std::string>& soundFilePath) { + soundFilePath_ = soundFilePath; +} + +boost::optional<std::string> HighlightAction::getSoundFilePath() const { + return soundFilePath_; +} + +void HighlightAction::setSystemNotificationEnabled(bool systemNotificationEnabled) { + systemNotificaitonEnabled_ = systemNotificationEnabled; +} + +bool HighlightAction::isSystemNotificationEnabled() const { + return systemNotificaitonEnabled_; +} + +bool HighlightAction::isEmpty() const { + return !frontColor_.is_initialized() && !backColor_.is_initialized() && !soundFilePath_.is_initialized() && !systemNotificaitonEnabled_; +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightAction.h b/Swift/Controllers/Highlighting/HighlightAction.h new file mode 100644 index 0000000..da92901 --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightAction.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> + +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/optional.hpp> +#include <boost/serialization/optional.hpp> + +namespace Swift { + class HighlightAction { + public: + void setFrontColor(const boost::optional<std::string>& frontColor); + boost::optional<std::string> getFrontColor() const; + + void setBackColor(const boost::optional<std::string>& backColor); + boost::optional<std::string> getBackColor() const; + + void setSoundFilePath(const boost::optional<std::string>& soundFilePath); + boost::optional<std::string> getSoundFilePath() const; + + void setSystemNotificationEnabled(bool systemNotificationEnabled); + bool isSystemNotificationEnabled() const; + + // @return returns true if the HighlightAction would result in no + // noticable action to the user. + bool isEmpty() const; + + private: + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + + private: + // Highlight color. + boost::optional<std::string> frontColor_; + boost::optional<std::string> backColor_; + + // Audio notification. + boost::optional<std::string> soundFilePath_; + + // macOS Notification Center or similar. + bool systemNotificaitonEnabled_ = false; + }; + + bool operator ==(const HighlightAction& a, const HighlightAction& b); + bool operator !=(const HighlightAction& a, const HighlightAction& b); + + template<class Archive> + void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) { + ar & frontColor_; + ar & backColor_; + ar & soundFilePath_; + ar & systemNotificaitonEnabled_; + } + +} diff --git a/Swift/Controllers/Highlighting/HighlightConfiguration.cpp b/Swift/Controllers/Highlighting/HighlightConfiguration.cpp new file mode 100644 index 0000000..e82adb8 --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightConfiguration.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> + +namespace Swift { + +bool operator ==(const HighlightConfiguration& a, const HighlightConfiguration& b) { + if (a.keywordHighlights != b.keywordHighlights) { + return false; + } + if (a.contactHighlights != b.contactHighlights) { + return false; + } + if (a.ownMentionAction != b.ownMentionAction) { + return false; + } + if (a.playSoundOnIncomingDirectMessages != b.playSoundOnIncomingDirectMessages) { + return false; + } + if (a.showNotificationOnIncomingDirectMessages != b.showNotificationOnIncomingDirectMessages) { + return false; + } + if (a.playSoundOnIncomingGroupchatMessages != b.playSoundOnIncomingGroupchatMessages) { + return false; + } + if (a.showNotificationOnIncomingGroupchatMessages != b.showNotificationOnIncomingGroupchatMessages) { + return false; + } + return true; +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightConfiguration.h b/Swift/Controllers/Highlighting/HighlightConfiguration.h new file mode 100644 index 0000000..d262dba --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightConfiguration.h @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> +#include <vector> + +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> + +#include <Swift/Controllers/Highlighting/HighlightAction.h> + +namespace Swift { + +class HighlightConfiguration { +public: + class ContactHighlight { + public: + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + + public: + std::string name; + HighlightAction action; + }; + + class KeywordHightlight { + public: + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + + public: + std::string keyword; + bool matchCaseSensitive = false; + HighlightAction action; + }; + + friend class boost::serialization::access; + template<class Archive> void serialize(Archive & ar, const unsigned int version); + +public: + std::vector<KeywordHightlight> keywordHighlights; + std::vector<ContactHighlight> contactHighlights; + HighlightAction ownMentionAction; + bool playSoundOnIncomingDirectMessages = false; + bool showNotificationOnIncomingDirectMessages = false; + bool playSoundOnIncomingGroupchatMessages = false; + bool showNotificationOnIncomingGroupchatMessages = false; +}; + +bool operator ==(HighlightConfiguration const& a, HighlightConfiguration const& b); + +template<class Archive> +void HighlightConfiguration::ContactHighlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & name; + ar & action; +} + +template<class Archive> +void HighlightConfiguration::KeywordHightlight::serialize(Archive& ar, const unsigned int /*version*/) { + ar & keyword; + ar & matchCaseSensitive; + ar & action; +} + +template<class Archive> +void HighlightConfiguration::serialize(Archive& ar, const unsigned int /*version*/) { + ar & keywordHighlights; + ar & contactHighlights; + ar & ownMentionAction; + ar & playSoundOnIncomingDirectMessages; + ar & showNotificationOnIncomingDirectMessages; + ar & playSoundOnIncomingGroupchatMessages; + ar & showNotificationOnIncomingGroupchatMessages; +} + +} diff --git a/Swift/Controllers/HighlightEditorController.cpp b/Swift/Controllers/Highlighting/HighlightEditorController.cpp index 1f5f928..50da3dc 100644 --- a/Swift/Controllers/HighlightEditorController.cpp +++ b/Swift/Controllers/Highlighting/HighlightEditorController.cpp @@ -5,12 +5,12 @@ */ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ -#include <Swift/Controllers/HighlightEditorController.h> +#include <Swift/Controllers/Highlighting/HighlightEditorController.h> #include <boost/bind.hpp> diff --git a/Swift/Controllers/HighlightEditorController.h b/Swift/Controllers/Highlighting/HighlightEditorController.h index a699751..d7608a5 100644 --- a/Swift/Controllers/HighlightEditorController.h +++ b/Swift/Controllers/Highlighting/HighlightEditorController.h @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2014-2016 Isode Limited. + * Copyright (c) 2014-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ diff --git a/Swift/Controllers/Highlighting/HighlightManager.cpp b/Swift/Controllers/Highlighting/HighlightManager.cpp new file mode 100644 index 0000000..89261af --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightManager.cpp @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/Highlighting/HighlightManager.h> + +#include <cassert> +#include <sstream> + +#include <boost/algorithm/string.hpp> +#include <boost/archive/text_iarchive.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/bind.hpp> +#include <boost/numeric/conversion/cast.hpp> +#include <boost/regex.hpp> +#include <boost/serialization/vector.hpp> + +#include <Swiften/Base/Log.h> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/Highlighting/Highlighter.h> +#include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> + +namespace Swift { + +HighlightManager::HighlightManager(SettingsProvider* settings) + : settings_(settings) + , storingSettings_(false) { + highlightConfiguration_ = std::make_shared<HighlightConfiguration>(); + loadSettings(); + handleSettingChangedConnection_ = settings_->onSettingChanged.connect(boost::bind(&HighlightManager::handleSettingChanged, this, _1)); +} + +void HighlightManager::handleSettingChanged(const std::string& settingPath) { + if (!storingSettings_ && SettingConstants::HIGHLIGHT_RULES.getKey() == settingPath) { + loadSettings(); + } +} + +HighlightConfiguration HighlightManager::getDefaultConfig() { + HighlightConfiguration defaultConfiguration; + defaultConfiguration.playSoundOnIncomingDirectMessages = true; + defaultConfiguration.showNotificationOnIncomingDirectMessages = true; + defaultConfiguration.ownMentionAction.setFrontColor(std::string("black")); + defaultConfiguration.ownMentionAction.setBackColor(std::string("yellow")); + defaultConfiguration.ownMentionAction.setSoundFilePath(std::string("/sounds/message-received.wav")); + defaultConfiguration.ownMentionAction.setSystemNotificationEnabled(true); + return defaultConfiguration; +} + +void HighlightManager::storeSettings() { + storingSettings_ = true; // don't reload settings while saving + settings_->storeSetting(SettingConstants::HIGHLIGHT_RULES_V2, highlightConfigurationToString(*highlightConfiguration_)); + storingSettings_ = false; +} + +void HighlightManager::loadSettings() { + std::string configString = settings_->getSetting(SettingConstants::HIGHLIGHT_RULES_V2); + *highlightConfiguration_ = highlightConfigurationFromString(configString); +} + +Highlighter* HighlightManager::createHighlighter(NickResolver* nickResolver) { + return new Highlighter(this, nickResolver); +} + +void HighlightManager::resetToDefaultConfiguration() { + *highlightConfiguration_ = getDefaultConfig(); +} + +HighlightConfiguration HighlightManager::highlightConfigurationFromString(const std::string& dataString) { + std::stringstream stream; + stream << dataString; + + HighlightConfiguration configuration; + try { + boost::archive::text_iarchive archive(stream); + archive >> configuration; + } + catch (boost::archive::archive_exception&) { + configuration = getDefaultConfig(); + SWIFT_LOG(warning) << "Failed to load highlight configuration. Will use default configuration instead." << std::endl; + } + return configuration; +} + +std::string HighlightManager::highlightConfigurationToString(const HighlightConfiguration& configuration) { + std::stringstream stream; + boost::archive::text_oarchive archive(stream); + archive & configuration; + return stream.str(); +} + +} diff --git a/Swift/Controllers/Highlighting/HighlightManager.h b/Swift/Controllers/Highlighting/HighlightManager.h new file mode 100644 index 0000000..13f59fa --- /dev/null +++ b/Swift/Controllers/Highlighting/HighlightManager.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. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <memory> +#include <string> + +#include <boost/signals2.hpp> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> + +namespace Swift { + + class NickResolver; + class SettingsProvider; + class Highlighter; + + class HighlightManager { + public: + HighlightManager(SettingsProvider* settings); + + Highlighter* createHighlighter(NickResolver* nickResolver); + + std::shared_ptr<HighlightConfiguration> getConfiguration() const { + return highlightConfiguration_; + } + + void setConfiguration(const HighlightConfiguration& config) { + *highlightConfiguration_ = config; + storeSettings(); + } + + void resetToDefaultConfiguration(); + + void storeSettings(); + void loadSettings(); + + boost::signals2::signal<void (const HighlightAction&)> onHighlight; + + private: + void handleSettingChanged(const std::string& settingPath); + + static HighlightConfiguration getDefaultConfig(); + + static HighlightConfiguration highlightConfigurationFromString(const std::string& dataString); + static std::string highlightConfigurationToString(const HighlightConfiguration& configuration); + + private: + SettingsProvider* settings_; + bool storingSettings_; + std::shared_ptr<HighlightConfiguration> highlightConfiguration_; + boost::signals2::scoped_connection handleSettingChangedConnection_; + }; +} diff --git a/Swift/Controllers/Highlighting/Highlighter.cpp b/Swift/Controllers/Highlighting/Highlighter.cpp new file mode 100644 index 0000000..b05de2d --- /dev/null +++ b/Swift/Controllers/Highlighting/Highlighter.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2012 Maciej Niedzielski + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/Controllers/Highlighting/Highlighter.h> + +#include <set> +#include <string> + +#include <Swiften/Base/String.h> +#include <Swiften/Base/format.h> +#include <Swiften/Client/NickResolver.h> + +#include <Swift/Controllers/Highlighting/HighlightManager.h> +#include <Swift/Controllers/Intl.h> +#include <Swift/Controllers/XMPPEvents/MessageEvent.h> + +namespace Swift { + +Highlighter::Highlighter(HighlightManager* manager, NickResolver* nickResolver) + : manager_(manager), nickResolver_(nickResolver) { +} + +void Highlighter::handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr<MessageEvent> event) { + if (std::shared_ptr<MessageEvent> messageEvent = std::dynamic_pointer_cast<MessageEvent>(event)) { + JID jid = messageEvent->getStanza()->getFrom(); + std::string nickname = nickResolver_->jidToNick(jid); + + std::string messageText = messageEvent->getStanza()->getBody().get_value_or(""); + if (boost::starts_with(messageText, "/me ")) { + messageText = "*" + String::getSplittedAtFirst(messageText, ' ').second + "*"; + } + + if (message.getHighlightActionDirectMessage().isSystemNotificationEnabled()) { + // title: Romeo says + // message: message + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% says")) % nickname); + event->addNotification(title, messageText); + } + if (message.getHighlightActionGroupMessage().isSystemNotificationEnabled()) { + // title: Romeo in $roomJID says + // message: message + std::string roomName = jid.getNode(); + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% in %2% says")) % nickname % roomName); + event->addNotification(title, messageText); + } + if (message.getHighlightActionOwnMention().isSystemNotificationEnabled()) { + // title: Romeo mentioned you in $roomJID + // message: message + std::string roomName = jid.getNode(); + std::string title = str(format(QT_TRANSLATE_NOOP("", "%1% mentioned you in %2%")) % nickname % roomName); + event->addNotification(title, messageText); + } + if (message.getHighlightActionSender().isSystemNotificationEnabled()) { + // title: Romeo says + // message: message + auto title = str(format(QT_TRANSLATE_NOOP("", "%1% says")) % nickname); + event->addNotification(title, messageText); + } + for (auto&& part : message.getParts()) { + auto highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part); + if (highlightPart && highlightPart->action.isSystemNotificationEnabled()) { + // title: Romeo mentioned '$keyword' + // message: message + auto title = str(format(QT_TRANSLATE_NOOP("", "%1% mentioned '%2%'")) % nickname % highlightPart->text); + event->addNotification(title, messageText); + } + } + } +} + +void Highlighter::handleSoundNotifications(const ChatWindow::ChatMessage& chatMessage) { + std::set<std::string> playedSoundPaths; + std::vector<HighlightAction> actionsToPlay; + + // collect unique sounds to play + auto checkSoundActionAndQueueUnique = [&](const HighlightAction& action) { + if (action.getSoundFilePath()) { + auto soundFilePath = action.getSoundFilePath().get_value_or(""); + if (playedSoundPaths.find(soundFilePath) == playedSoundPaths.end()) { + playedSoundPaths.insert(soundFilePath); + actionsToPlay.push_back(action); + } + } + }; + + for (auto&& part : chatMessage.getParts()) { + auto highlightMessage = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part); + if (highlightMessage) { + checkSoundActionAndQueueUnique(highlightMessage->action); + } + } + + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionSender()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionOwnMention()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionDirectMessage()); + checkSoundActionAndQueueUnique(chatMessage.getHighlightActionGroupMessage()); + + // play sounds + for (const auto& action : actionsToPlay) { + manager_->onHighlight(action); + } +} + +} diff --git a/Swift/Controllers/Highlighting/Highlighter.h b/Swift/Controllers/Highlighting/Highlighter.h new file mode 100644 index 0000000..51308a1 --- /dev/null +++ b/Swift/Controllers/Highlighting/Highlighter.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. + */ + +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/XMPPEvents/MessageEvent.h> + +namespace Swift { + + class HighlightManager; + class NickResolver; + + class Highlighter { + public: + Highlighter(HighlightManager* manager, NickResolver* nickResolver); + + void handleSystemNotifications(const ChatWindow::ChatMessage& message, std::shared_ptr<MessageEvent> event); + void handleSoundNotifications(const ChatWindow::ChatMessage& chatMessage); + + private: + HighlightManager* manager_; + NickResolver* nickResolver_; + }; + +} diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index 0d9f1b8..e64b23d 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -57,8 +57,8 @@ #include <Swift/Controllers/EventWindowController.h> #include <Swift/Controllers/FileTransfer/FileTransferOverview.h> #include <Swift/Controllers/FileTransferListController.h> -#include <Swift/Controllers/HighlightEditorController.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightEditorController.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/HistoryController.h> #include <Swift/Controllers/HistoryViewController.h> #include <Swift/Controllers/Intl.h> diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 105b44b..0c3127c 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -22,90 +22,88 @@ if env["SCONS_STAGE"] == "build" : myenv.UseFlags(env["SWIFTEN_FLAGS"]) myenv.UseFlags(env["SWIFTEN_DEP_FLAGS"]) myenv.StaticLibrary("SwiftControllers", [ + "AdHocController.cpp", + "AdHocManager.cpp", + "BlockListController.cpp", "Chat/ChatController.cpp", "Chat/ChatControllerBase.cpp", + "Chat/ChatMessageParser.cpp", "Chat/ChatsManager.cpp", "Chat/MUCController.cpp", "Chat/MUCSearchController.cpp", "Chat/UserSearchController.cpp", - "Chat/ChatMessageParser.cpp", - "ContactSuggester.cpp", - "MainController.cpp", - "ProfileController.cpp", - "ShowProfileController.cpp", + "ChatMessageSummarizer.cpp", + "Contact.cpp", "ContactEditController.cpp", + "ContactProvider.cpp", + "ContactSuggester.cpp", + "ContactsFromXMPPRoster.cpp", + "EventNotifier.cpp", + "EventWindowController.cpp", "FileTransfer/FileTransferController.cpp", "FileTransfer/FileTransferOverview.cpp", "FileTransfer/FileTransferProgressInfo.cpp", - "Roster/RosterController.cpp", - "Roster/RosterGroupExpandinessPersister.cpp", + "FileTransferListController.cpp", + "Highlighting/HighlightAction.cpp", + "Highlighting/HighlightEditorController.cpp", + "Highlighting/HighlightManager.cpp", + "Highlighting/Highlighter.cpp", + "HistoryController.cpp", + "HistoryViewController.cpp", + "MainController.cpp", + "PresenceNotifier.cpp", + "PreviousStatusStore.cpp", + "ProfileController.cpp", + "ProfileSettingsProvider.cpp", "Roster/ContactRosterItem.cpp", "Roster/GroupRosterItem.cpp", - "Roster/RosterItem.cpp", "Roster/Roster.cpp", + "Roster/RosterController.cpp", + "Roster/RosterGroupExpandinessPersister.cpp", + "Roster/RosterItem.cpp", "Roster/RosterVCardProvider.cpp", "Roster/TableRoster.cpp", - "EventWindowController.cpp", - "SoundEventController.cpp", - "SystemTrayController.cpp", - "XMLConsoleController.cpp", - "HistoryViewController.cpp", - "HistoryController.cpp", - "FileTransferListController.cpp", - "BlockListController.cpp", - "StatusTracker.cpp", - "PresenceNotifier.cpp", - "EventNotifier.cpp", - "AdHocManager.cpp", - "AdHocController.cpp", - "XMPPEvents/EventController.cpp", - "UIEvents/UIEvent.cpp", - "UIInterfaces/XMLConsoleWidget.cpp", - "UIInterfaces/ChatListWindow.cpp", - "UIInterfaces/HighlightEditorWindow.cpp", - "PreviousStatusStore.cpp", - "ProfileSettingsProvider.cpp", + "SettingConstants.cpp", "Settings/SettingsProviderHierachy.cpp", "Settings/XMLSettingsProvider.cpp", - "Storages/CertificateStorageFactory.cpp", - "Storages/CertificateStorage.cpp", + "ShowProfileController.cpp", + "SoundEventController.cpp", + "StatusCache.cpp", + "StatusTracker.cpp", + "StatusUtil.cpp", + "Storages/AvatarFileStorage.cpp", + "Storages/CapsFileStorage.cpp", "Storages/CertificateFileStorage.cpp", "Storages/CertificateMemoryStorage.cpp", - "Storages/AvatarFileStorage.cpp", + "Storages/CertificateStorage.cpp", + "Storages/CertificateStorageFactory.cpp", "Storages/FileStorages.cpp", "Storages/RosterFileStorage.cpp", - "Storages/CapsFileStorage.cpp", "Storages/VCardFileStorage.cpp", - "StatusUtil.cpp", + "SystemTrayController.cpp", "Translator.cpp", - "XMPPURIController.cpp", - "ChatMessageSummarizer.cpp", - "SettingConstants.cpp", + "UIEvents/UIEvent.cpp", + "UIInterfaces/ChatListWindow.cpp", + "UIInterfaces/HighlightEditorWindow.cpp", + "UIInterfaces/XMLConsoleWidget.cpp", "WhiteboardManager.cpp", - "StatusCache.cpp", - "HighlightAction.cpp", - "HighlightEditorController.cpp", - "HighlightManager.cpp", - "HighlightRule.cpp", - "Highlighter.cpp", - "ContactsFromXMPPRoster.cpp", - "ContactProvider.cpp", - "Contact.cpp" + "XMLConsoleController.cpp", + "XMPPEvents/EventController.cpp", + "XMPPURIController.cpp", ]) env.Append(UNITTEST_SOURCES = [ - File("Roster/UnitTest/RosterControllerTest.cpp"), - File("Roster/UnitTest/RosterTest.cpp"), - File("Roster/UnitTest/LeastCommonSubsequenceTest.cpp"), - File("Roster/UnitTest/TableRosterTest.cpp"), - File("UnitTest/PreviousStatusStoreTest.cpp"), - File("UnitTest/PresenceNotifierTest.cpp"), + File("Chat/UnitTest/ChatMessageParserTest.cpp"), File("Chat/UnitTest/ChatsManagerTest.cpp"), File("Chat/UnitTest/MUCControllerTest.cpp"), - File("Chat/UnitTest/ChatMessageParserTest.cpp"), - File("UnitTest/MockChatWindow.cpp"), - File("UnitTest/ChatMessageSummarizerTest.cpp"), + File("Roster/UnitTest/LeastCommonSubsequenceTest.cpp"), + File("Roster/UnitTest/RosterControllerTest.cpp"), + File("Roster/UnitTest/RosterTest.cpp"), + File("Roster/UnitTest/TableRosterTest.cpp"), File("Settings/UnitTest/SettingsProviderHierachyTest.cpp"), - File("UnitTest/HighlightRuleTest.cpp"), - File("UnitTest/ContactSuggesterTest.cpp") + File("UnitTest/ChatMessageSummarizerTest.cpp"), + File("UnitTest/ContactSuggesterTest.cpp"), + File("UnitTest/MockChatWindow.cpp"), + File("UnitTest/PresenceNotifierTest.cpp"), + File("UnitTest/PreviousStatusStoreTest.cpp"), ]) diff --git a/Swift/Controllers/SettingConstants.cpp b/Swift/Controllers/SettingConstants.cpp index dedf56b..f0064ba 100644 --- a/Swift/Controllers/SettingConstants.cpp +++ b/Swift/Controllers/SettingConstants.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -20,6 +20,7 @@ const SettingsProvider::Setting<bool> SettingConstants::SHOW_OFFLINE("showOfflin const SettingsProvider::Setting<std::string> SettingConstants::EXPANDED_ROSTER_GROUPS("GroupExpandiness", ""); const SettingsProvider::Setting<bool> SettingConstants::PLAY_SOUNDS("playSounds", true); const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES("highlightRules", "@"); +const SettingsProvider::Setting<std::string> SettingConstants::HIGHLIGHT_RULES_V2("highlightRulesV2", "@"); const SettingsProvider::Setting<std::string> SettingConstants::INVITE_AUTO_ACCEPT_MODE("inviteAutoAcceptMode", "presence"); const SettingsProvider::Setting<bool> SettingConstants::DISCONNECT_ON_CARD_REMOVAL("disconnectOnCardRemoval", true); const SettingsProvider::Setting<bool> SettingConstants::SINGLE_SIGN_ON("singleSignOn", false); diff --git a/Swift/Controllers/SettingConstants.h b/Swift/Controllers/SettingConstants.h index 3f15c44..fec2d27 100644 --- a/Swift/Controllers/SettingConstants.h +++ b/Swift/Controllers/SettingConstants.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2016 Isode Limited. + * Copyright (c) 2012-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -58,6 +58,13 @@ namespace Swift { */ static const SettingsProvider::Setting<std::string> HIGHLIGHT_RULES; /** + * The #HIGHLIGHT_RULES_V2 setting specifies the second version of highlight configuration + * rules, incompatible to old highlight rules. + * + * Its value is a Boost serialized representation. + */ + static const SettingsProvider::Setting<std::string> HIGHLIGHT_RULES_V2; + /** * The #INVITE_AUTO_ACCEPT_MODE setting specifies how to handle invites to chat rooms. * * Supported values are: diff --git a/Swift/Controllers/SoundEventController.cpp b/Swift/Controllers/SoundEventController.cpp index 5c7568f..2bafcca 100644 --- a/Swift/Controllers/SoundEventController.cpp +++ b/Swift/Controllers/SoundEventController.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -8,7 +8,7 @@ #include <boost/bind.hpp> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> #include <Swift/Controllers/SettingConstants.h> #include <Swift/Controllers/SoundPlayer.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> @@ -37,8 +37,8 @@ void SoundEventController::handleEventQueueEventAdded(std::shared_ptr<StanzaEven } void SoundEventController::handleHighlight(const HighlightAction& action) { - if (playSounds_ && action.playSound()) { - soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFile()); + if (playSounds_ && action.getSoundFilePath()) { + soundPlayer_->playSound(SoundPlayer::MessageReceived, action.getSoundFilePath().get_value_or("")); } } diff --git a/Swift/Controllers/SoundEventController.h b/Swift/Controllers/SoundEventController.h index e5b43b4..d612b18 100644 --- a/Swift/Controllers/SoundEventController.h +++ b/Swift/Controllers/SoundEventController.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -8,7 +8,7 @@ #include <memory> -#include <Swift/Controllers/HighlightAction.h> +#include <Swift/Controllers/Highlighting/HighlightAction.h> #include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/XMPPEvents/StanzaEvent.h> @@ -21,10 +21,13 @@ namespace Swift { 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(std::shared_ptr<StanzaEvent> event); void handleHighlight(const HighlightAction& action); + + private: EventController* eventController_; SoundPlayer* soundPlayer_; bool playSounds_; diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h index 8ee083d..7aaa90e 100644 --- a/Swift/Controllers/UIInterfaces/ChatWindow.h +++ b/Swift/Controllers/UIInterfaces/ChatWindow.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -22,7 +22,7 @@ #include <Swiften/Elements/SecurityLabelsCatalog.h> #include <Swiften/MUC/MUCBookmark.h> -#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighting/HighlightManager.h> namespace Swift { class AvatarManager; @@ -62,12 +62,36 @@ namespace Swift { parts_ = parts; } - void setFullMessageHighlightAction(const HighlightAction& action) { - fullMessageHighlightAction_ = action; + void setHighlightActionSender(const HighlightAction& action) { + highlightActionSender_ = action; } - const HighlightAction& getFullMessageHighlightAction() const { - return fullMessageHighlightAction_; + const HighlightAction& getHighlightActionSender() const { + return highlightActionSender_; + } + + void setHighlightActionOwnMention(const HighlightAction& action) { + highlightActionOwnMention_ = action; + } + + const HighlightAction& getHighlightActionOwnMention() const { + return highlightActionOwnMention_; + } + + void setHighlightActionGroupMessage(const HighlightAction& action) { + highlightActionGroupMessage_ = action; + } + + const HighlightAction& getHighlightActionGroupMessage() const { + return highlightActionGroupMessage_; + } + + void setHighlightActonDirectMessage(const HighlightAction& action) { + highlightActionDirectMessage_ = action; + } + + const HighlightAction& getHighlightActionDirectMessage() const { + return highlightActionDirectMessage_; } bool isMeCommand() const { @@ -80,7 +104,10 @@ namespace Swift { private: std::vector<std::shared_ptr<ChatMessagePart> > parts_; - HighlightAction fullMessageHighlightAction_; + HighlightAction highlightActionSender_; + HighlightAction highlightActionOwnMention_; + HighlightAction highlightActionGroupMessage_; + HighlightAction highlightActionDirectMessage_; bool isMeCommand_ = false; }; diff --git a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp b/Swift/Controllers/UnitTest/HighlightRuleTest.cpp deleted file mode 100644 index 8d49d5d..0000000 --- a/Swift/Controllers/UnitTest/HighlightRuleTest.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <string> -#include <vector> - -#include <cppunit/extensions/HelperMacros.h> -#include <cppunit/extensions/TestFactoryRegistry.h> - -#include <Swift/Controllers/HighlightRule.h> - -using namespace Swift; - -class HighlightRuleTest : public CppUnit::TestFixture { - CPPUNIT_TEST_SUITE(HighlightRuleTest); - CPPUNIT_TEST(testEmptyRuleNeverMatches); - CPPUNIT_TEST(testKeyword); - CPPUNIT_TEST(testNickKeyword); - CPPUNIT_TEST(testNickWithoutOtherKeywords); - CPPUNIT_TEST(testSender); - CPPUNIT_TEST(testSenderAndKeyword); - CPPUNIT_TEST(testWholeWords); - CPPUNIT_TEST(testCase); - CPPUNIT_TEST(testWholeWordsAndCase); - CPPUNIT_TEST(testMUC); - CPPUNIT_TEST_SUITE_END(); - - public: - void setUp() { - std::vector<std::string> keywords; - keywords.push_back("keyword1"); - keywords.push_back("KEYWORD2"); - - std::vector<std::string> senders; - senders.push_back("sender1"); - senders.push_back("SENDER2"); - - emptyRule = new HighlightRule(); - - keywordRule = new HighlightRule(); - keywordRule->setKeywords(keywords); - - keywordChatRule = new HighlightRule(); - keywordChatRule->setKeywords(keywords); - keywordChatRule->setMatchChat(true); - - keywordNickChatRule = new HighlightRule(); - keywordNickChatRule->setKeywords(keywords); - keywordNickChatRule->setNickIsKeyword(true); - keywordNickChatRule->setMatchChat(true); - - nickChatRule = new HighlightRule(); - nickChatRule->setNickIsKeyword(true); - nickChatRule->setMatchChat(true); - - nickRule = new HighlightRule(); - nickRule->setNickIsKeyword(true); - - senderRule = new HighlightRule(); - senderRule->setSenders(senders); - - senderChatRule = new HighlightRule(); - senderChatRule->setSenders(senders); - senderChatRule->setMatchChat(true); - - senderKeywordChatRule = new HighlightRule(); - senderKeywordChatRule->setSenders(senders); - senderKeywordChatRule->setKeywords(keywords); - senderKeywordChatRule->setMatchChat(true); - - senderKeywordNickChatRule = new HighlightRule(); - senderKeywordNickChatRule->setSenders(senders); - senderKeywordNickChatRule->setKeywords(keywords); - senderKeywordNickChatRule->setNickIsKeyword(true); - senderKeywordNickChatRule->setMatchChat(true); - - senderKeywordNickWordChatRule = new HighlightRule(); - senderKeywordNickWordChatRule->setSenders(senders); - senderKeywordNickWordChatRule->setKeywords(keywords); - senderKeywordNickWordChatRule->setNickIsKeyword(true); - senderKeywordNickWordChatRule->setMatchWholeWords(true); - senderKeywordNickWordChatRule->setMatchChat(true); - - senderKeywordNickCaseChatRule = new HighlightRule(); - senderKeywordNickCaseChatRule->setSenders(senders); - senderKeywordNickCaseChatRule->setKeywords(keywords); - senderKeywordNickCaseChatRule->setNickIsKeyword(true); - senderKeywordNickCaseChatRule->setMatchCase(true); - senderKeywordNickCaseChatRule->setMatchChat(true); - - senderKeywordNickCaseWordChatRule = new HighlightRule(); - senderKeywordNickCaseWordChatRule->setSenders(senders); - senderKeywordNickCaseWordChatRule->setKeywords(keywords); - senderKeywordNickCaseWordChatRule->setNickIsKeyword(true); - senderKeywordNickCaseWordChatRule->setMatchCase(true); - senderKeywordNickCaseWordChatRule->setMatchWholeWords(true); - senderKeywordNickCaseWordChatRule->setMatchChat(true); - - senderKeywordNickMUCRule = new HighlightRule(); - senderKeywordNickMUCRule->setSenders(senders); - senderKeywordNickMUCRule->setKeywords(keywords); - senderKeywordNickMUCRule->setNickIsKeyword(true); - senderKeywordNickMUCRule->setMatchMUC(true); - } - - void tearDown() { - delete emptyRule; - - delete keywordRule; - delete keywordChatRule; - delete keywordNickChatRule; - delete nickChatRule; - delete nickRule; - - delete senderRule; - delete senderChatRule; - delete senderKeywordChatRule; - delete senderKeywordNickChatRule; - - delete senderKeywordNickWordChatRule; - delete senderKeywordNickCaseChatRule; - delete senderKeywordNickCaseWordChatRule; - - delete senderKeywordNickMUCRule; - } - - void testEmptyRuleNeverMatches() { - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "from", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("body", "", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "from", "", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(emptyRule->isMatch("", "", "", HighlightRule::MUCMessage), false); - } - - void testKeyword() { - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body", "sender contains keyword1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc keyword1 xyz", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abckeyword1xyz", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("KEYword1", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abc KEYword1 xyz", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("abcKEYword1xyz", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("keyword2", "from", "nick", HighlightRule::ChatMessage), true); - } - - void testNickKeyword() { - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(keywordChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "sender contains nick", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body contains mixed-case NiCk", "sender", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(keywordNickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), false); - } - - void testNickWithoutOtherKeywords() { - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains nick", "from", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(nickRule->isMatch("body contains nick", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "sender contains nick but it does't matter", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body contains mixed-case NiCk", "from", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("nickname", "from", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("NIckNAME", "from", "nick", HighlightRule::ChatMessage), true); - - // there are no keywords in this rule and empty nick is not treated as a keyword, so we don't check for keywords to get a match - CPPUNIT_ASSERT_EQUAL(nickChatRule->isMatch("body", "from", "", HighlightRule::ChatMessage), true); - } - - void testSender() { - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "from", "nick", HighlightRule::MUCMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender1", "nick", HighlightRule::MUCMessage), false); - CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderRule->isMatch("body contains sender1", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc sender1 xyz", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcsender1xyz", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "SENDer1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abc SENDer1 xyz", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "abcSENDer1xyz", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderChatRule->isMatch("body", "sender2", "nick", HighlightRule::ChatMessage), true); - } - - void testSenderAndKeyword() { - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - } - - void testWholeWords() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), true); - } - - void testCase() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("xkeyword1", "xsender1", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), true); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false); - } - - void testWholeWordsAndCase() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "from", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("xkeyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "xsender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::ChatMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains xnick", "sender1", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("keyword1", "SENDer1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("KEYword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickCaseWordChatRule->isMatch("body contains NiCk", "sender1", "nick", HighlightRule::ChatMessage), false); - } - - void testMUC() { - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body", "from", "nick", HighlightRule::ChatMessage), false); - - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::ChatMessage), false); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("keyword1", "sender1", "nick", HighlightRule::MUCMessage), true); - CPPUNIT_ASSERT_EQUAL(senderKeywordNickMUCRule->isMatch("body contains nick", "sender1", "nick", HighlightRule::MUCMessage), true); - } - - private: - HighlightRule* emptyRule; - - HighlightRule* keywordRule; - HighlightRule* keywordChatRule; - HighlightRule* keywordNickChatRule; - HighlightRule* nickChatRule; - HighlightRule* nickRule; - - HighlightRule* senderRule; - HighlightRule* senderChatRule; - HighlightRule* senderKeywordChatRule; - HighlightRule* senderKeywordNickChatRule; - - HighlightRule* senderKeywordNickWordChatRule; - HighlightRule* senderKeywordNickCaseChatRule; - HighlightRule* senderKeywordNickCaseWordChatRule; - - HighlightRule* senderKeywordNickMUCRule; -}; - -CPPUNIT_TEST_SUITE_REGISTRATION(HighlightRuleTest); diff --git a/Swift/Controllers/XMPPEvents/MessageEvent.h b/Swift/Controllers/XMPPEvents/MessageEvent.h index 7af2be6..12f4c48 100644 --- a/Swift/Controllers/XMPPEvents/MessageEvent.h +++ b/Swift/Controllers/XMPPEvents/MessageEvent.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -16,6 +16,17 @@ namespace Swift { class MessageEvent : public StanzaEvent { public: + class SystemNotification { + public: + SystemNotification(const std::string& title, const std::string& message) : title(title), message(message) { + } + + public: + std::string title; + std::string message; + }; + + public: typedef std::shared_ptr<MessageEvent> ref; MessageEvent(std::shared_ptr<Message> stanza) : stanza_(stanza), targetsMe_(true) {} @@ -26,6 +37,14 @@ namespace Swift { return getStanza()->isError() || !getStanza()->getBody().get_value_or("").empty(); } + void addNotification(const std::string& title, const std::string& message) { + systemNotifications_.push_back(SystemNotification(title, message)); + } + + const std::vector<SystemNotification>& getNotifications() const { + return systemNotifications_; + } + void read() { assert (isReadable()); conclude(); @@ -41,6 +60,7 @@ namespace Swift { private: std::shared_ptr<Message> stanza_; + std::vector<SystemNotification> systemNotifications_; bool targetsMe_; }; } diff --git a/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp b/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp new file mode 100644 index 0000000..5f71ed4 --- /dev/null +++ b/Swift/QtUI/QtCheckBoxStyledItemDelegate.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtCheckBoxStyledItemDelegate.h> + +#include <array> + +#include <QApplication> +#include <QEvent> +#include <QPainter> + +namespace Swift { + +QtCheckBoxStyledItemDelegate::QtCheckBoxStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtCheckBoxStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + QStyleOptionButton cbOpt; + cbOpt.rect = option.rect; + cbOpt.state = QStyle::State_Active; + + auto copyFlags = std::array<QStyle::StateFlag, 2>({{QStyle::State_Enabled/*, QStyle::State_Sunken*/}}); + for (auto flag : copyFlags) { + if (option.state && flag) { + cbOpt.state |= flag; + } + } + + bool isChecked = index.data(DATA_ROLE).toBool(); + if (isChecked) { + cbOpt.state |= QStyle::State_On; + } + else { + cbOpt.state |= QStyle::State_Off; + } + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + QApplication::style()->drawControl(QStyle::CE_CheckBox, &cbOpt, painter); +} + +bool QtCheckBoxStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + model->setData(index, !index.data(DATA_ROLE).toBool(), DATA_ROLE); + } + return true; +} + +}; diff --git a/Swift/QtUI/QtCheckBoxStyledItemDelegate.h b/Swift/QtUI/QtCheckBoxStyledItemDelegate.h new file mode 100644 index 0000000..1d8db62 --- /dev/null +++ b/Swift/QtUI/QtCheckBoxStyledItemDelegate.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +namespace Swift { + +class QtCheckBoxStyledItemDelegate : public QStyledItemDelegate { + public: + QtCheckBoxStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + public: + static const int DATA_ROLE = Qt::UserRole + 1; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp b/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp new file mode 100644 index 0000000..c3fef5a --- /dev/null +++ b/Swift/QtUI/QtColorSelectionStyledItemDelegate.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtColorSelectionStyledItemDelegate.h> + +#include <QApplication> +#include <QColorDialog> +#include <QEvent> +#include <QPainter> +#include <QStyleOptionComboBox> + +namespace Swift { + +QtColorSelectionStyledItemDelegate::QtColorSelectionStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtColorSelectionStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + // draw item selected background + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + + // draw combo box + QStyleOptionComboBox opt; + opt.rect = option.rect; + opt.state = QStyle::State_Active | QStyle::State_Enabled; + QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &opt, painter); + + // draw currently selected color + auto contentRect = QApplication::style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField); + auto currentColor = index.data(DATA_ROLE).value<QColor>(); + painter->save(); + painter->setClipRect(contentRect); + painter->fillRect(contentRect.adjusted(1, -4, -1, -3), currentColor); + painter->restore(); +} + +bool QtColorSelectionStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + auto currentColor = index.data(DATA_ROLE).value<QColor>(); + auto newColor = QColorDialog::getColor(currentColor); + if (newColor.isValid()) { + model->setData(index, newColor, DATA_ROLE); + } + + auto correspondingDialog = qobject_cast<QDialog*>(parent()); + if (correspondingDialog) { + correspondingDialog->raise(); + } + } + return true; +} + +}; diff --git a/Swift/QtUI/QtColorSelectionStyledItemDelegate.h b/Swift/QtUI/QtColorSelectionStyledItemDelegate.h new file mode 100644 index 0000000..d6b3336 --- /dev/null +++ b/Swift/QtUI/QtColorSelectionStyledItemDelegate.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +namespace Swift { + +class QtColorSelectionStyledItemDelegate : public QStyledItemDelegate { + public: + QtColorSelectionStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + public: + static const int DATA_ROLE = Qt::UserRole + 1; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtHighlightEditor.cpp b/Swift/QtUI/QtHighlightEditor.cpp deleted file mode 100644 index dd95941..0000000 --- a/Swift/QtUI/QtHighlightEditor.cpp +++ /dev/null @@ -1,568 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#include <Swift/QtUI/QtHighlightEditor.h> - -#include <cassert> - -#include <boost/lexical_cast.hpp> - -#include <QFileDialog> -#include <QScrollBar> -#include <QTreeWidgetItem> - -#include <Swift/Controllers/HighlightManager.cpp> - -#include <Swift/QtUI/QtSettingsProvider.h> -#include <Swift/QtUI/QtSwiftUtil.h> -#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> - -namespace Swift { - -QtHighlightEditor::QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent) - : QWidget(parent), settings_(settings), previousRow_(-1) -{ - ui_.setupUi(this); - - connect(ui_.listWidget, SIGNAL(currentRowChanged(int)), SLOT(onCurrentRowChanged(int))); - - connect(ui_.newButton, SIGNAL(clicked()), SLOT(onNewButtonClicked())); - connect(ui_.deleteButton, SIGNAL(clicked()), SLOT(onDeleteButtonClicked())); - - connect(ui_.buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(onApplyButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), SLOT(onCancelButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(onOkButtonClick())); - connect(ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked()), SLOT(onResetToDefaultRulesClicked())); - - connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect())); - connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(colorCustomSelect())); - - connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); - connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); - connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(soundCustomSelect())); - - /* replace the static line-edit control with the roster autocompleter */ - ui_.dummySenderName->setVisible(false); - jid_ = new QtSuggestingJIDInput(this, settings); - ui_.senderName->addWidget(jid_); - jid_->onUserSelected.connect(boost::bind(&QtHighlightEditor::handleOnUserSelected, this, _1)); - - /* handle autocomplete */ - connect(jid_, SIGNAL(textEdited(QString)), SLOT(handleContactSuggestionRequested(QString))); - - /* we need to be notified if any of the state changes so that we can update our textual rule description */ - connect(ui_.chatRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.roomRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.nickIsKeyword, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.allMsgRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.senderRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(jid_, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); - connect(ui_.keywordRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.keyword, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); - connect(ui_.matchPartialWords, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.matchCase, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); - - /* allow selection of a custom sound file */ - connect(ui_.soundFileButton, SIGNAL(clicked()), SLOT(selectSoundFile())); - - /* allowing reordering items */ - connect(ui_.moveUpButton, SIGNAL(clicked()), this, SLOT(onUpButtonClicked())); - connect(ui_.moveDownButton, SIGNAL(clicked()), this, SLOT(onDownButtonClicked())); - - setWindowTitle(tr("Highlight Rules")); -} - -QtHighlightEditor::~QtHighlightEditor() -{ -} - -QString QtHighlightEditor::formatShortDescription(const HighlightRule &rule) -{ - const QString chatOrRoom = (rule.getMatchChat() ? tr("chat") : tr("room")); - - std::vector<std::string> senders = rule.getSenders(); - std::vector<std::string> keywords = rule.getKeywords(); - - if (senders.empty() && keywords.empty() && !rule.getNickIsKeyword()) { - return tr("All %1 messages.").arg(chatOrRoom); - } - - if (rule.getNickIsKeyword()) { - return tr("All %1 messages that mention my name.").arg(chatOrRoom); - } - - if (!senders.empty()) { - return tr("All %1 messages from %2.").arg(chatOrRoom, P2QSTRING(senders[0])); - } - - if (!keywords.empty()) { - return tr("All %1 messages mentioning the keyword '%2'.").arg(chatOrRoom, P2QSTRING(keywords[0])); - } - - return tr("Unknown Rule"); -} - -void QtHighlightEditor::show() -{ - highlightManager_->loadSettings(); - - populateList(); - - if (ui_.listWidget->count()) { - selectRow(0); - } - - updateResetToDefaultRulesVisibility(); - - /* prepare default states */ - widgetClick(); - - QWidget::show(); - QWidget::activateWindow(); -} - -void QtHighlightEditor::setHighlightManager(HighlightManager* highlightManager) -{ - highlightManager_ = highlightManager; -} - -void QtHighlightEditor::setContactSuggestions(const std::vector<Contact::ref>& suggestions) -{ - jid_->setSuggestions(suggestions); -} - -void QtHighlightEditor::colorOtherSelect() -{ - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); -} - -void QtHighlightEditor::colorCustomSelect() -{ - ui_.foregroundColor->setEnabled(true); - ui_.backgroundColor->setEnabled(true); -} - -void QtHighlightEditor::soundOtherSelect() -{ - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); -} - -void QtHighlightEditor::soundCustomSelect() -{ - ui_.soundFile->setEnabled(true); - ui_.soundFileButton->setEnabled(true); -} - -void QtHighlightEditor::onNewButtonClicked() -{ - int row = getSelectedRow() + 1; - populateList(); - HighlightRule newRule; - newRule.setMatchMUC(true); - highlightManager_->insertRule(row, newRule); - QListWidgetItem *item = new QListWidgetItem(); - item->setText(formatShortDescription(newRule)); - QFont font; - font.setItalic(true); - item->setFont(font); - ui_.listWidget->insertItem(row, item); - selectRow(row); -} - -void QtHighlightEditor::onDeleteButtonClicked() -{ - int selectedRow = getSelectedRow(); - assert(selectedRow>=0 && selectedRow<ui_.listWidget->count()); - delete ui_.listWidget->takeItem(selectedRow); - highlightManager_->removeRule(selectedRow); - - if (!ui_.listWidget->count()) { - disableDialog(); - ui_.deleteButton->setEnabled(false); - } else { - if (selectedRow == ui_.listWidget->count()) { - selectRow(ui_.listWidget->count() - 1); - } else { - selectRow(selectedRow); - } - } -} - -void QtHighlightEditor::moveRowFromTo(int fromRow, int toRow) { - int verticalScrollAreaPosition = ui_.scrollArea->verticalScrollBar()->value(); - highlightManager_->swapRules(fromRow, toRow); - populateList(); - selectRow(toRow); - ui_.scrollArea->verticalScrollBar()->setValue(verticalScrollAreaPosition); -} - -void QtHighlightEditor::onUpButtonClicked() { - const size_t moveFrom = ui_.listWidget->currentRow(); - const size_t moveTo = moveFrom - 1; - moveRowFromTo(moveFrom, moveTo); -} - -void QtHighlightEditor::onDownButtonClicked() { - const size_t moveFrom = ui_.listWidget->currentRow(); - const size_t moveTo = moveFrom + 1; - moveRowFromTo(moveFrom, moveTo); -} - -void QtHighlightEditor::onCurrentRowChanged(int currentRow) -{ - ui_.deleteButton->setEnabled(currentRow != -1); - ui_.moveUpButton->setEnabled(currentRow != -1 && currentRow != 0); - ui_.moveDownButton->setEnabled(currentRow != -1 && currentRow != (ui_.listWidget->count()-1)); - - if (previousRow_ != -1) { - if (ui_.listWidget->count() > previousRow_) { - QFont font; - font.setItalic(false); - ui_.listWidget->item(previousRow_)->setFont(font); - highlightManager_->setRule(previousRow_, ruleFromDialog()); - } - } - - if (currentRow != -1) { - HighlightRule rule = highlightManager_->getRule(currentRow); - ruleToDialog(rule); - if (ui_.listWidget->currentItem()) { - QFont font; - font.setItalic(true); - ui_.listWidget->currentItem()->setFont(font); - ui_.listWidget->currentItem()->setText(formatShortDescription(rule)); - } - } - - /* grey the dialog if we have nothing selected */ - if (currentRow == -1) { - disableDialog(); - } - - previousRow_ = currentRow; - - updateResetToDefaultRulesVisibility(); -} - -void QtHighlightEditor::onApplyButtonClick() -{ - selectRow(getSelectedRow()); /* force save */ - highlightManager_->storeSettings(); -} - -void QtHighlightEditor::onCancelButtonClick() -{ - close(); -} - -void QtHighlightEditor::onOkButtonClick() -{ - onApplyButtonClick(); - close(); -} - -void QtHighlightEditor::setChildWidgetStates() -{ - /* disable appropriate radio button child widgets */ - - if (ui_.chatRadio->isChecked()) { - if (ui_.nickIsKeyword->isChecked()) { - /* switch to another choice before we disable this button */ - ui_.allMsgRadio->setChecked(true); - } - ui_.nickIsKeyword->setEnabled(false); - } else if (ui_.roomRadio->isChecked()) { - ui_.nickIsKeyword->setEnabled(true); - } else { /* chats and rooms */ - ui_.nickIsKeyword->setEnabled(true); - } - - if (ui_.senderRadio->isChecked()) { - jid_->setEnabled(true); - } else { - jid_->setEnabled(false); - } - - if (ui_.keywordRadio->isChecked()) { - ui_.keyword->setEnabled(true); - ui_.matchPartialWords->setEnabled(true); - ui_.matchCase->setEnabled(true); - } else { - ui_.keyword->setEnabled(false); - ui_.matchPartialWords->setEnabled(false); - ui_.matchCase->setEnabled(false); - } - - if (ui_.chatRadio->isChecked()) { - ui_.allMsgRadio->setText(tr("Apply to all chat messages")); - } else { - ui_.allMsgRadio->setText(tr("Apply to all room messages")); - } -} - -void QtHighlightEditor::widgetClick() -{ - setChildWidgetStates(); - - HighlightRule rule = ruleFromDialog(); - - if (ui_.listWidget->currentItem()) { - ui_.listWidget->currentItem()->setText(formatShortDescription(rule)); - } -} - -void QtHighlightEditor::disableDialog() -{ - ui_.chatRadio->setEnabled(false); - ui_.roomRadio->setEnabled(false); - ui_.allMsgRadio->setEnabled(false); - ui_.nickIsKeyword->setEnabled(false); - ui_.senderRadio->setEnabled(false); - ui_.dummySenderName->setEnabled(false); - ui_.keywordRadio->setEnabled(false); - ui_.keyword->setEnabled(false); - ui_.matchPartialWords->setEnabled(false); - ui_.matchCase->setEnabled(false); - ui_.noColorRadio->setEnabled(false); - ui_.customColorRadio->setEnabled(false); - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); - ui_.noSoundRadio->setEnabled(false); - ui_.defaultSoundRadio->setEnabled(false); - ui_.customSoundRadio->setEnabled(false); - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); -} - -void QtHighlightEditor::handleContactSuggestionRequested(const QString& text) -{ - std::string stdText = Q2PSTRING(text); - onContactSuggestionsRequested(stdText); -} - -void QtHighlightEditor::selectSoundFile() -{ - QString path = QFileDialog::getOpenFileName(this, tr("Select sound file..."), QString(), tr("Sounds (*.wav)")); - ui_.soundFile->setText(path); -} - -void QtHighlightEditor::onResetToDefaultRulesClicked() { - highlightManager_->resetToDefaultRulesList(); - populateList(); - updateResetToDefaultRulesVisibility(); -} - -void QtHighlightEditor::handleOnUserSelected(const Contact::ref& contact) { - /* this might seem like it should be standard behaviour for the suggesting input box, but is not desirable in all cases */ - if (contact->jid.isValid()) { - jid_->setText(P2QSTRING(contact->jid.toString())); - } else { - jid_->setText(P2QSTRING(contact->name)); - } -} - -void QtHighlightEditor::populateList() -{ - previousRow_ = -1; - ui_.listWidget->clear(); - HighlightRulesListPtr rules = highlightManager_->getRules(); - for (size_t i = 0; i < rules->getSize(); ++i) { - const HighlightRule& rule = rules->getRule(i); - QListWidgetItem *item = new QListWidgetItem(); - item->setText(formatShortDescription(rule)); - ui_.listWidget->addItem(item); - } -} - -void QtHighlightEditor::selectRow(int row) -{ - for (int i = 0; i < ui_.listWidget->count(); ++i) { - if (i == row) { - ui_.listWidget->item(i)->setSelected(true); - onCurrentRowChanged(i); - } else { - ui_.listWidget->item(i)->setSelected(false); - } - } - ui_.listWidget->setCurrentRow(row); -} - -int QtHighlightEditor::getSelectedRow() const -{ - for (int i = 0; i < ui_.listWidget->count(); ++i) { - if (ui_.listWidget->item(i)->isSelected()) { - return i; - } - } - return -1; -} - -HighlightRule QtHighlightEditor::ruleFromDialog() -{ - HighlightRule rule; - HighlightAction& action = rule.getAction(); - - if (ui_.chatRadio->isChecked()) { - rule.setMatchChat(true); - rule.setMatchMUC(false); - } else { - rule.setMatchChat(false); - rule.setMatchMUC(true); - } - - if (ui_.allMsgRadio->isChecked()) { - action.setHighlightWholeMessage(true); - } - - if (ui_.senderRadio->isChecked()) { - QString senderName = jid_->text(); - if (!senderName.isEmpty()) { - std::vector<std::string> senders; - senders.push_back(Q2PSTRING(senderName)); - rule.setSenders(senders); - action.setHighlightWholeMessage(true); - } - } - - if (ui_.keywordRadio->isChecked()) { - QString keywordString = ui_.keyword->text(); - if (!keywordString.isEmpty()) { - std::vector<std::string> keywords; - keywords.push_back(Q2PSTRING(keywordString)); - rule.setKeywords(keywords); - } - } - - if (ui_.nickIsKeyword->isChecked()) { - rule.setNickIsKeyword(true); - rule.setMatchWholeWords(true); - rule.setMatchCase(true); - } else { - rule.setMatchWholeWords(!ui_.matchPartialWords->isChecked()); - rule.setMatchCase(ui_.matchCase->isChecked()); - } - - if (ui_.noColorRadio->isChecked()) { - action.setTextColor(""); - action.setTextBackground(""); - } else { - action.setTextColor(Q2PSTRING(ui_.foregroundColor->getColor().name())); - action.setTextBackground(Q2PSTRING(ui_.backgroundColor->getColor().name())); - } - - if (ui_.noSoundRadio->isChecked()) { - action.setPlaySound(false); - } else if (ui_.defaultSoundRadio->isChecked()) { - action.setPlaySound(true); - action.setSoundFile(""); - } else { - action.setPlaySound(true); - action.setSoundFile(Q2PSTRING(ui_.soundFile->text())); - } - - return rule; -} - -void QtHighlightEditor::ruleToDialog(const HighlightRule& rule) -{ - ui_.chatRadio->setEnabled(true); - ui_.roomRadio->setEnabled(true); - - if (rule.getMatchMUC()) { - ui_.chatRadio->setChecked(false); - ui_.roomRadio->setChecked(true); - } else { - ui_.chatRadio->setChecked(true); - ui_.roomRadio->setChecked(false); - } - - ui_.allMsgRadio->setEnabled(true); - ui_.allMsgRadio->setChecked(true); /* this is the default radio button */ - jid_->setText(""); - ui_.keyword->setText(""); - ui_.matchPartialWords->setChecked(false); - ui_.matchCase->setChecked(false); - - ui_.nickIsKeyword->setEnabled(true); - if (rule.getNickIsKeyword()) { - ui_.nickIsKeyword->setChecked(true); - } - - ui_.senderRadio->setEnabled(true); - std::vector<std::string> senders = rule.getSenders(); - if (!senders.empty()) { - ui_.senderRadio->setChecked(true); - jid_->setText(P2QSTRING(senders[0])); - } - - ui_.keywordRadio->setEnabled(true); - std::vector<std::string> keywords = rule.getKeywords(); - if (!keywords.empty()) { - ui_.keywordRadio->setChecked(true); - ui_.keyword->setText(P2QSTRING(keywords[0])); - ui_.matchPartialWords->setChecked(!rule.getMatchWholeWords()); - ui_.matchCase->setChecked(rule.getMatchCase()); - } - - const HighlightAction& action = rule.getAction(); - - ui_.noColorRadio->setEnabled(true); - ui_.customColorRadio->setEnabled(true); - if (action.getTextColor().empty() && action.getTextBackground().empty()) { - ui_.noColorRadio->setChecked(true); - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); - } else { - ui_.foregroundColor->setEnabled(true); - ui_.backgroundColor->setEnabled(true); - QColor foregroundColor(P2QSTRING(action.getTextColor())); - ui_.foregroundColor->setColor(foregroundColor); - QColor backgroundColor(P2QSTRING(action.getTextBackground())); - ui_.backgroundColor->setColor(backgroundColor); - ui_.customColorRadio->setChecked(true); - } - - ui_.noSoundRadio->setEnabled(true); - ui_.defaultSoundRadio->setEnabled(true); - ui_.customSoundRadio->setEnabled(true); - ui_.soundFile->setText(""); - ui_.soundFile->setEnabled(false); - ui_.soundFileButton->setEnabled(false); - if (action.playSound()) { - if (action.getSoundFile().empty()) { - ui_.defaultSoundRadio->setChecked(true); - } else { - ui_.customSoundRadio->setChecked(true); - ui_.soundFile->setText(P2QSTRING(action.getSoundFile())); - ui_.soundFile->setEnabled(true); - ui_.soundFileButton->setEnabled(true); - } - } else { - ui_.noSoundRadio->setChecked(true); - } - - /* set radio button child option states */ - setChildWidgetStates(); -} - -void QtHighlightEditor::updateResetToDefaultRulesVisibility() { - ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults)->setVisible(!highlightManager_->isDefaultRulesList()); -} - -} diff --git a/Swift/QtUI/QtHighlightEditor.h b/Swift/QtUI/QtHighlightEditor.h deleted file mode 100644 index c4a12e2..0000000 --- a/Swift/QtUI/QtHighlightEditor.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2012 Maciej Niedzielski - * Licensed under the simplified BSD license. - * See Documentation/Licenses/BSD-simplified.txt for more information. - */ - -/* - * Copyright (c) 2014-2016 Isode Limited. - * All rights reserved. - * See the COPYING file for more information. - */ - -#pragma once - -#include <Swift/Controllers/HighlightRule.h> -#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h> - -#include <Swift/QtUI/ui_QtHighlightEditor.h> - -namespace Swift { - - class QtSettingsProvider; - class QtSuggestingJIDInput; - class QtWebKitChatView; - - class QtHighlightEditor : public QWidget, public HighlightEditorWindow { - Q_OBJECT - - public: - QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent = nullptr); - virtual ~QtHighlightEditor(); - - virtual void show(); - virtual void setHighlightManager(HighlightManager* highlightManager); - virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions); - - private slots: - void colorOtherSelect(); - void colorCustomSelect(); - void soundOtherSelect(); - void soundCustomSelect(); - void onNewButtonClicked(); - void onDeleteButtonClicked(); - void onUpButtonClicked(); - void onDownButtonClicked(); - void onCurrentRowChanged(int currentRow); - void onApplyButtonClick(); - void onCancelButtonClick(); - void onOkButtonClick(); - void setChildWidgetStates(); - void widgetClick(); - void disableDialog(); - void handleContactSuggestionRequested(const QString& text); - void selectSoundFile(); - void onResetToDefaultRulesClicked(); - - private: - QString formatShortDescription(const HighlightRule &rule); - void handleOnUserSelected(const Contact::ref& contact); - void populateList(); - void selectRow(int row); - int getSelectedRow() const; - HighlightRule ruleFromDialog(); - void ruleToDialog(const HighlightRule& rule); - void updateResetToDefaultRulesVisibility(); - void moveRowFromTo(int fromRow, int toRow); - - private: - Ui::QtHighlightEditor ui_; - QtSettingsProvider* settings_; - HighlightManager* highlightManager_ = nullptr; - QtSuggestingJIDInput* jid_; - int previousRow_; - }; - -} diff --git a/Swift/QtUI/QtHighlightEditor.ui b/Swift/QtUI/QtHighlightEditor.ui deleted file mode 100644 index 6d2338d..0000000 --- a/Swift/QtUI/QtHighlightEditor.ui +++ /dev/null @@ -1,466 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>QtHighlightEditor</class> - <widget class="QWidget" name="QtHighlightEditor"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>500</width> - <height>600</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>500</width> - <height>600</height> - </size> - </property> - <property name="windowTitle"> - <string>Form</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> - <item> - <widget class="QScrollArea" name="scrollArea"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="frameShape"> - <enum>QFrame::StyledPanel</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Sunken</enum> - </property> - <property name="widgetResizable"> - <bool>true</bool> - </property> - <widget class="QWidget" name="scrollAreaWidgetContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>463</width> - <height>792</height> - </rect> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <item> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>Incoming messages are checked against the following rules. First rule that matches will be executed.</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QListWidget" name="listWidget"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>0</height> - </size> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <item> - <spacer name="horizontalSpacer_8"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="newButton"> - <property name="text"> - <string>New Rule</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="deleteButton"> - <property name="text"> - <string>Remove Rule</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="moveUpButton"> - <property name="text"> - <string>Move Up</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="moveDownButton"> - <property name="text"> - <string>Move Down</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="Line" name="line_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Apply Rule To</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QRadioButton" name="roomRadio"> - <property name="text"> - <string>Rooms</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="chatRadio"> - <property name="text"> - <string>Chats</string> - </property> - <property name="checked"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>246</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_6"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Rule Conditions</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QRadioButton" name="allMsgRadio"> - <property name="text"> - <string>Apply to all messages</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="nickIsKeyword"> - <property name="text"> - <string>Only messages mentioning me</string> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="senderRadio"> - <property name="text"> - <string>Messages from this sender:</string> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="senderName"> - <property name="sizeConstraint"> - <enum>QLayout::SetMinimumSize</enum> - </property> - <item> - <widget class="QLineEdit" name="dummySenderName"/> - </item> - </layout> - </item> - <item> - <widget class="QRadioButton" name="keywordRadio"> - <property name="text"> - <string>Messages containing this keyword:</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="keyword"/> - </item> - <item> - <widget class="QCheckBox" name="matchPartialWords"> - <property name="text"> - <string>Match keyword within longer words</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="matchCase"> - <property name="text"> - <string>Keyword is case sensitive</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Highlight Action</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_5"> - <item> - <widget class="QRadioButton" name="noColorRadio"> - <property name="text"> - <string>No Highlight</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="customColorRadio"> - <property name="text"> - <string>Custom Color</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="Swift::QtColorToolButton" name="foregroundColor"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>&Text</string> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - <item> - <widget class="Swift::QtColorToolButton" name="backgroundColor"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>&Background</string> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_4"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="title"> - <string>Sound Action</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <item> - <widget class="QRadioButton" name="noSoundRadio"> - <property name="text"> - <string>No Sound</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="defaultSoundRadio"> - <property name="text"> - <string>Default Sound</string> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="customSoundRadio"> - <property name="text"> - <string>Custom Sound</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_8"> - <item> - <spacer name="horizontalSpacer_3"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLineEdit" name="soundFile"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="readOnly"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="soundFileButton"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="text"> - <string>...</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_9"> - <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <customwidgets> - <customwidget> - <class>Swift::QtColorToolButton</class> - <extends>QToolButton</extends> - <header>QtColorToolButton.h</header> - </customwidget> - </customwidgets> - <resources/> - <connections/> -</ui> diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp new file mode 100644 index 0000000..c4e64ab --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.cpp @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtHighlightNotificationConfigDialog.h> + +#include <Swiften/Base/Log.h> + +#include <Swift/Controllers/Highlighting/HighlightManager.h> + +#include <Swift/QtUI/QtCheckBoxStyledItemDelegate.h> +#include <Swift/QtUI/QtColorSelectionStyledItemDelegate.h> +#include <Swift/QtUI/QtSoundSelectionStyledItemDelegate.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +QtHighlightNotificationConfigDialog::QtHighlightNotificationConfigDialog(QtSettingsProvider* settings, QWidget* parent) : QDialog(parent), settings_(settings) { + ui_.setupUi(this); + + // setup custom delegates for checkboxes, color selection, and sound selection + ui_.userHighlightTreeWidget->setItemDelegateForColumn(1, new QtColorSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(2, new QtColorSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(3, new QtSoundSelectionStyledItemDelegate(this)); + ui_.userHighlightTreeWidget->setItemDelegateForColumn(4, new QtCheckBoxStyledItemDelegate(this)); + + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(1, new QtCheckBoxStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(2, new QtColorSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(3, new QtColorSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(4, new QtSoundSelectionStyledItemDelegate(this)); + ui_.keywordHighlightTreeWidget->setItemDelegateForColumn(5, new QtCheckBoxStyledItemDelegate(this)); + + // user highlight edit slots + connect(ui_.addUserHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::EditRole, ""); + item->setData(1, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#000000")); + item->setData(2, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#ffff00")); + item->setData(3, Qt::EditRole, ""); + item->setData(4, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(true)); + ui_.userHighlightTreeWidget->addTopLevelItem(item); + }); + connect(ui_.removeUserHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto currentItem = ui_.userHighlightTreeWidget->currentItem(); + if (currentItem) { + ui_.userHighlightTreeWidget->takeTopLevelItem(ui_.userHighlightTreeWidget->indexOfTopLevelItem(currentItem)); + } + }); + connect(ui_.userHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { + ui_.removeUserHighlightPushButton->setEnabled(current != 0); + }); + + // keyword highlight edit slots + connect(ui_.addKeywordHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::EditRole, ""); + item->setData(1, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(false)); + item->setData(2, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#000000")); + item->setData(3, QtColorSelectionStyledItemDelegate::DATA_ROLE, QColor("#ffff00")); + item->setData(4, Qt::EditRole, ""); + item->setData(5, QtCheckBoxStyledItemDelegate::DATA_ROLE, QVariant(true)); + ui_.keywordHighlightTreeWidget->addTopLevelItem(item); + }); + connect(ui_.removeKeywordHighlightPushButton, &QPushButton::clicked, [&](bool) { + auto currentItem = ui_.keywordHighlightTreeWidget->currentItem(); + if (currentItem) { + ui_.keywordHighlightTreeWidget->takeTopLevelItem(ui_.keywordHighlightTreeWidget->indexOfTopLevelItem(currentItem)); + } + }); + connect(ui_.keywordHighlightTreeWidget, &QTreeWidget::currentItemChanged, [&](QTreeWidgetItem* current, QTreeWidgetItem* ) { + ui_.removeKeywordHighlightPushButton->setEnabled(current != 0); + }); + + // setup slots for main dialog buttons + connect(ui_.buttonBox, &QDialogButtonBox::clicked, [&](QAbstractButton* clickedButton) { + if (clickedButton == ui_.buttonBox->button(QDialogButtonBox::RestoreDefaults)) { + if (highlightManager_) { + highlightManager_->resetToDefaultConfiguration(); + setHighlightConfigurationToDialog(*highlightManager_->getConfiguration()); + } + } + }); + connect(this, &QDialog::accepted, [&]() { + if (highlightManager_) { + highlightManager_->setConfiguration(getHighlightConfigurationFromDialog()); + } + }); +} + +QtHighlightNotificationConfigDialog::~QtHighlightNotificationConfigDialog() { +} + +void QtHighlightNotificationConfigDialog::show() { + if (highlightManager_) { + setHighlightConfigurationToDialog(*(highlightManager_->getConfiguration())); + } + QWidget::show(); + QWidget::activateWindow(); +} + +void QtHighlightNotificationConfigDialog::setHighlightManager(HighlightManager* highlightManager) { + highlightManager_ = highlightManager; +} + +void QtHighlightNotificationConfigDialog::setContactSuggestions(const std::vector<Contact::ref>& /*suggestions*/) { + +} + +HighlightConfiguration QtHighlightNotificationConfigDialog::getHighlightConfigurationFromDialog() const { + auto qtColorToOptionalString = [&](const QColor& color) { + boost::optional<std::string> colorString; + if (color.isValid()) { + colorString = Q2PSTRING(color.name(QColor::HexRgb)); + } + return colorString; + }; + + auto getHighlightActionFromWidgetItem = [&](const QTreeWidgetItem* item, int startingColumn) { + HighlightAction action; + + action.setFrontColor(qtColorToOptionalString(item->data(startingColumn, QtColorSelectionStyledItemDelegate::DATA_ROLE).value<QColor>())); + action.setBackColor(qtColorToOptionalString(item->data(startingColumn + 1, QtColorSelectionStyledItemDelegate::DATA_ROLE).value<QColor>())); + + std::string soundFilePath = Q2PSTRING(item->data(startingColumn + 2, Qt::EditRole).toString()); + if (soundFilePath == "defaultSound") { + action.setSoundFilePath(boost::optional<std::string>("")); + } + else if (soundFilePath.empty()) { + action.setSoundFilePath(boost::optional<std::string>()); + } + else { + action.setSoundFilePath(boost::optional<std::string>(soundFilePath)); + } + + action.setSystemNotificationEnabled(item->data(startingColumn + 3, QtCheckBoxStyledItemDelegate::DATA_ROLE).toBool()); + return action; + }; + + HighlightConfiguration uiConfiguration; + + // contact highlights + for (int i = 0; i < ui_.userHighlightTreeWidget->topLevelItemCount(); i++) { + auto item = ui_.userHighlightTreeWidget->topLevelItem(i); + HighlightConfiguration::ContactHighlight contactHighlight; + contactHighlight.name = Q2PSTRING(item->data(0, Qt::EditRole).toString()); + contactHighlight.action = getHighlightActionFromWidgetItem(item, 1); + uiConfiguration.contactHighlights.push_back(contactHighlight); + } + + // keyword highlights + for (int i = 0; i < ui_.keywordHighlightTreeWidget->topLevelItemCount(); i++) { + auto item = ui_.keywordHighlightTreeWidget->topLevelItem(i); + HighlightConfiguration::KeywordHightlight keywordHighlight; + keywordHighlight.keyword = Q2PSTRING(item->data(0, Qt::EditRole).toString()); + keywordHighlight.matchCaseSensitive = item->data(1, QtCheckBoxStyledItemDelegate::DATA_ROLE).toBool(); + keywordHighlight.action = getHighlightActionFromWidgetItem(item, 2); + uiConfiguration.keywordHighlights.push_back(keywordHighlight); + } + + // general configuration + uiConfiguration.playSoundOnIncomingDirectMessages = ui_.playSoundOnDirectMessagesCheckBox->isChecked(); + uiConfiguration.showNotificationOnIncomingDirectMessages = ui_.notificationOnDirectMessagesCheckBox->isChecked(); + uiConfiguration.playSoundOnIncomingGroupchatMessages = ui_.playSoundOnGroupMessagesCheckBox->isChecked(); + uiConfiguration.showNotificationOnIncomingGroupchatMessages = ui_.notificationOnGroupMessagesCheckBox->isChecked(); + + uiConfiguration.ownMentionAction.setFrontColor(qtColorToOptionalString(ui_.mentionTextColorColorButton->getColor())); + uiConfiguration.ownMentionAction.setBackColor(qtColorToOptionalString(ui_.mentionBackgroundColorButton->getColor())); + uiConfiguration.ownMentionAction.setSoundFilePath(ui_.playSoundOnMentionCheckBox->isChecked() ? boost::optional<std::string>("") : boost::optional<std::string>()); + uiConfiguration.ownMentionAction.setSystemNotificationEnabled(ui_.notificationOnMentionCheckBox->isChecked()); + return uiConfiguration; +} + +void QtHighlightNotificationConfigDialog::setHighlightConfigurationToDialog(const HighlightConfiguration& config) { + auto optionalStringToQtColor = [](const boost::optional<std::string>& colorString) { + QColor qtColor; + if (colorString) { + qtColor = QColor(P2QSTRING(colorString.get_value_or(std::string("")))); + } + return qtColor; + }; + + auto optionalSoundPathStringToQString = [](const boost::optional<std::string>& soundPath) { + QString ret; + if (soundPath) { + if (soundPath.get_value_or("").empty()) { + ret = "defaultSound"; + } + else { + ret = P2QSTRING(soundPath.get_value_or("")); + } + } + return ret; + }; + + auto setHighlightActionOnTreeWidgetItem = [&](QTreeWidgetItem* item, int startingColumn, const HighlightAction& action) { + item->setData(startingColumn, QtColorSelectionStyledItemDelegate::DATA_ROLE, optionalStringToQtColor(action.getFrontColor())); + item->setData(startingColumn + 1, QtColorSelectionStyledItemDelegate::DATA_ROLE, optionalStringToQtColor(action.getBackColor())); + item->setData(startingColumn + 2, Qt::DisplayRole, P2QSTRING(action.getSoundFilePath().get_value_or(std::string("")))); + item->setData(startingColumn + 2, Qt::EditRole, optionalSoundPathStringToQString(action.getSoundFilePath())); + item->setData(startingColumn + 3, QtCheckBoxStyledItemDelegate::DATA_ROLE, action.isSystemNotificationEnabled()); + }; + + // contact highlights + ui_.userHighlightTreeWidget->clear(); + for (const auto& contactHighlight : config.contactHighlights) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + item->setData(0, Qt::DisplayRole, P2QSTRING(contactHighlight.name)); + item->setData(0, Qt::EditRole, P2QSTRING(contactHighlight.name)); + + setHighlightActionOnTreeWidgetItem(item, 1, contactHighlight.action); + + ui_.userHighlightTreeWidget->addTopLevelItem(item); + } + + // keyword highlights + ui_.keywordHighlightTreeWidget->clear(); + for (const auto& keywordHighlight : config.keywordHighlights) { + auto item = new QTreeWidgetItem(); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + item->setData(0, Qt::DisplayRole, P2QSTRING(keywordHighlight.keyword)); + item->setData(0, Qt::EditRole, P2QSTRING(keywordHighlight.keyword)); + item->setData(1, QtCheckBoxStyledItemDelegate::DATA_ROLE, keywordHighlight.matchCaseSensitive); + + setHighlightActionOnTreeWidgetItem(item, 2, keywordHighlight.action); + + ui_.keywordHighlightTreeWidget->addTopLevelItem(item); + } + + // general configuration + ui_.playSoundOnDirectMessagesCheckBox->setChecked(config.playSoundOnIncomingDirectMessages); + ui_.notificationOnDirectMessagesCheckBox->setChecked(config.showNotificationOnIncomingDirectMessages); + ui_.playSoundOnGroupMessagesCheckBox->setChecked(config.playSoundOnIncomingGroupchatMessages); + ui_.notificationOnGroupMessagesCheckBox->setChecked(config.showNotificationOnIncomingGroupchatMessages); + + ui_.mentionTextColorColorButton->setColor(optionalStringToQtColor(config.ownMentionAction.getFrontColor())); + ui_.mentionBackgroundColorButton->setColor(optionalStringToQtColor(config.ownMentionAction.getBackColor())); + ui_.playSoundOnMentionCheckBox->setChecked(config.ownMentionAction.getSoundFilePath().is_initialized()); + ui_.notificationOnMentionCheckBox->setChecked(config.ownMentionAction.isSystemNotificationEnabled()); +} + +} diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.h b/Swift/QtUI/QtHighlightNotificationConfigDialog.h new file mode 100644 index 0000000..03044eb --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <Swift/Controllers/Highlighting/HighlightConfiguration.h> +#include <Swift/Controllers/UIInterfaces/HighlightEditorWindow.h> + +#include <Swift/QtUI/ui_QtHighlightNotificationConfigDialog.h> + +namespace Swift { + + class QtSettingsProvider; + class QtSuggestingJIDInput; + + class QtHighlightNotificationConfigDialog : public QDialog, public HighlightEditorWindow { + Q_OBJECT + + public: + QtHighlightNotificationConfigDialog(QtSettingsProvider* settings, QWidget* parent = nullptr); + virtual ~QtHighlightNotificationConfigDialog(); + + virtual void show(); + virtual void setHighlightManager(HighlightManager* highlightManager); + virtual void setContactSuggestions(const std::vector<Contact::ref>& suggestions); + + private: + HighlightConfiguration getHighlightConfigurationFromDialog() const; + void setHighlightConfigurationToDialog(const HighlightConfiguration& config); + + private: + Ui::QtHighlightNotificationConfigDialog ui_; + QtSettingsProvider* settings_; + HighlightManager* highlightManager_ = nullptr; + QtSuggestingJIDInput* jid_ = nullptr; + }; + +} diff --git a/Swift/QtUI/QtHighlightNotificationConfigDialog.ui b/Swift/QtUI/QtHighlightNotificationConfigDialog.ui new file mode 100644 index 0000000..7074ad8 --- /dev/null +++ b/Swift/QtUI/QtHighlightNotificationConfigDialog.ui @@ -0,0 +1,537 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtHighlightNotificationConfigDialog</class> + <widget class="QDialog" name="QtHighlightNotificationConfigDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>629</width> + <height>515</height> + </rect> + </property> + <property name="windowTitle"> + <string>Highlight and Notification Configuration</string> + </property> + <property name="sizeGripEnabled"> + <bool>false</bool> + </property> + <property name="modal"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Highlight messages from these people</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeWidget" name="userHighlightTreeWidget"> + <property name="tabKeyNavigation"> + <bool>true</bool> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="defaultDropAction"> + <enum>Qt::MoveAction</enum> + </property> + <property name="indentation"> + <number>0</number> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="animated"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>false</bool> + </property> + <property name="expandsOnDoubleClick"> + <bool>false</bool> + </property> + <attribute name="headerCascadingSectionResizes"> + <bool>false</bool> + </attribute> + <attribute name="headerDefaultSectionSize"> + <number>80</number> + </attribute> + <attribute name="headerHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="headerMinimumSectionSize"> + <number>15</number> + </attribute> + <column> + <property name="text"> + <string>User</string> + </property> + </column> + <column> + <property name="text"> + <string>Text color</string> + </property> + </column> + <column> + <property name="text"> + <string>Background color</string> + </property> + </column> + <column> + <property name="text"> + <string>Play sound</string> + </property> + </column> + <column> + <property name="text"> + <string>Create notification</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>12</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="addUserHighlightPushButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="sizeIncrement"> + <size> + <width>1</width> + <height>1</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>13</pointsize> + <weight>50</weight> + <italic>false</italic> + <bold>false</bold> + <underline>false</underline> + <strikeout>false</strikeout> + <kerning>true</kerning> + </font> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeUserHighlightPushButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Highlight messages containing these keywords</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>6</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QTreeWidget" name="keywordHighlightTreeWidget"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>300</height> + </size> + </property> + <property name="tabKeyNavigation"> + <bool>true</bool> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="dragEnabled"> + <bool>true</bool> + </property> + <property name="dragDropMode"> + <enum>QAbstractItemView::InternalMove</enum> + </property> + <property name="defaultDropAction"> + <enum>Qt::MoveAction</enum> + </property> + <property name="indentation"> + <number>0</number> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="animated"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Keyword</string> + </property> + </column> + <column> + <property name="text"> + <string>Match case sensitive</string> + </property> + </column> + <column> + <property name="text"> + <string>Text color</string> + </property> + </column> + <column> + <property name="text"> + <string>Background color</string> + </property> + </column> + <column> + <property name="text"> + <string>Play sound</string> + </property> + </column> + <column> + <property name="text"> + <string>Create notification</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="spacing"> + <number>12</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="addKeywordHighlightPushButton"> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>+</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeKeywordHighlightPushButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="maximumSize"> + <size> + <width>26</width> + <height>26</height> + </size> + </property> + <property name="text"> + <string>-</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>General notification settings</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="Swift::QtColorToolButton" name="mentionBackgroundColorButton"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Highlight background color on own mention</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="1"> + <widget class="QCheckBox" name="notificationOnGroupMessagesCheckBox"> + <property name="text"> + <string>Create notification on incoming group messages</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="notificationOnMentionCheckBox"> + <property name="text"> + <string>Create notification when my name is mentioned</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QCheckBox" name="playSoundOnDirectMessagesCheckBox"> + <property name="text"> + <string>Play sound on incoming direct messages</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="playSoundOnGroupMessagesCheckBox"> + <property name="text"> + <string>Play sound on incoming group messages</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="notificationOnDirectMessagesCheckBox"> + <property name="text"> + <string>Create notification on incoming direct messages</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="playSoundOnMentionCheckBox"> + <property name="text"> + <string>Play sound when my name is mentioned</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="Swift::QtColorToolButton" name="mentionTextColorColorButton"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Highlight text color on own mention</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults</set> + </property> + <property name="centerButtons"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>Swift::QtColorToolButton</class> + <extends>QToolButton</extends> + <header>QtColorToolButton.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>userHighlightTreeWidget</tabstop> + <tabstop>addUserHighlightPushButton</tabstop> + <tabstop>removeUserHighlightPushButton</tabstop> + <tabstop>keywordHighlightTreeWidget</tabstop> + <tabstop>addKeywordHighlightPushButton</tabstop> + <tabstop>removeKeywordHighlightPushButton</tabstop> + <tabstop>playSoundOnDirectMessagesCheckBox</tabstop> + <tabstop>notificationOnDirectMessagesCheckBox</tabstop> + <tabstop>playSoundOnGroupMessagesCheckBox</tabstop> + <tabstop>notificationOnGroupMessagesCheckBox</tabstop> + <tabstop>playSoundOnMentionCheckBox</tabstop> + <tabstop>notificationOnMentionCheckBox</tabstop> + <tabstop>mentionTextColorColorButton</tabstop> + <tabstop>mentionBackgroundColorButton</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QtHighlightNotificationConfigDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QtHighlightNotificationConfigDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp new file mode 100644 index 0000000..3811004 --- /dev/null +++ b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#include <Swift/QtUI/QtSoundSelectionStyledItemDelegate.h> + +#include <QApplication> +#include <QComboBox> +#include <QEvent> +#include <QFileDialog> +#include <QMenu> +#include <QMouseEvent> +#include <QPainter> +#include <QStyle> +#include <QStyleOptionComboBox> + +#include <Swiften/Base/Log.h> + +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +QtSoundSelectionStyledItemDelegate::QtSoundSelectionStyledItemDelegate(QObject* parent) : QStyledItemDelegate(parent) { + +} + +void QtSoundSelectionStyledItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + // draw item selected background + painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + + auto editRoleString = index.data(Qt::EditRole).toString(); + + // draw combo box + QStyleOptionComboBox opt; + opt.rect = option.rect; + opt.rect.setHeight(opt.rect.height() + 2); + opt.state = QStyle::State_Active | QStyle::State_Enabled; + if (editRoleString.isEmpty()) { + opt.currentText = tr("No sound"); + } + else if (editRoleString == "defaultSound") { + opt.currentText = tr("Default sound"); + } + else { + opt.currentText = editRoleString; + } + + painter->save(); + QFont smallFont; + smallFont.setPointSize(smallFont.pointSize() - 3); + painter->setFont(smallFont); + + QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &opt, painter); + QApplication::style()->drawControl(QStyle::CE_ComboBoxLabel, &opt, painter); + painter->restore(); +} + +bool QtSoundSelectionStyledItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& /*option*/, const QModelIndex& index) { + if (event->type() == QEvent::MouseButtonRelease) { + auto mouseEvent = dynamic_cast<QMouseEvent*>(event); + assert(mouseEvent); + auto editRoleString = index.data(Qt::EditRole).toString(); + + auto popUpMenu = new QMenu(); + + auto noSound = popUpMenu->addAction(tr("No sound")); + auto defaultSound = popUpMenu->addAction(tr("Default sound")); + QAction* customSoundFile = nullptr; + QAction* selectedAction = nullptr; + if (editRoleString.isEmpty()) { + selectedAction = noSound; + } + else if (editRoleString == "defaultSound") { + selectedAction = defaultSound; + } + else { + customSoundFile = popUpMenu->addAction(editRoleString); + selectedAction = customSoundFile; + } + if (selectedAction) { + selectedAction->setCheckable(true); + selectedAction->setChecked(true); + } + auto chooseSoundFile = popUpMenu->addAction(tr("Choose sound fileā¦")); + + selectedAction = popUpMenu->exec(mouseEvent->globalPos(), selectedAction); + + if (selectedAction == defaultSound) { + model->setData(index, "defaultSound", Qt::EditRole); + } + else if (customSoundFile && (selectedAction == customSoundFile)) { + model->setData(index, customSoundFile->text(), Qt::EditRole); + } + else if (selectedAction == noSound) { + model->setData(index, "", Qt::EditRole); + } + else if (selectedAction == chooseSoundFile) { + auto newPath = QFileDialog::getOpenFileName(0, tr("Choose notification sound file"), "", tr("WAV Files (*.wav)")); + if (!newPath.isEmpty()) { + model->setData(index, newPath, Qt::EditRole); + } + } + + delete popUpMenu; + } + return true; +} + +}; diff --git a/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h new file mode 100644 index 0000000..fabf668 --- /dev/null +++ b/Swift/QtUI/QtSoundSelectionStyledItemDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +namespace Swift { + +class QtSoundSelectionStyledItemDelegate : public QStyledItemDelegate { + public: + QtSoundSelectionStyledItemDelegate(QObject* parent = nullptr); + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const; + + protected: + virtual bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index); +}; + +} diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 16952a0..ece29ec 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -23,7 +23,7 @@ #include <Swift/QtUI/QtChatWindowFactory.h> #include <Swift/QtUI/QtContactEditWindow.h> #include <Swift/QtUI/QtFileTransferListWidget.h> -#include <Swift/QtUI/QtHighlightEditor.h> +#include <Swift/QtUI/QtHighlightNotificationConfigDialog.h> #include <Swift/QtUI/QtHistoryWindow.h> #include <Swift/QtUI/QtJoinMUCWindow.h> #include <Swift/QtUI/QtLoginWindow.h> @@ -169,7 +169,7 @@ WhiteboardWindow* QtUIFactory::createWhiteboardWindow(std::shared_ptr<Whiteboard } HighlightEditorWindow* QtUIFactory::createHighlightEditorWindow() { - return new QtHighlightEditor(qtOnlySettings); + return new QtHighlightNotificationConfigDialog(qtOnlySettings); } BlockListEditorWidget *QtUIFactory::createBlockListEditorWidget() { diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp index 6fe9397..9aeef24 100644 --- a/Swift/QtUI/QtWebKitChatView.cpp +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2016 Isode Limited. + * Copyright (c) 2010-2017 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -478,7 +478,7 @@ std::string QtWebKitChatView::addMessage( std::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) { - return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, message.getFullMessageHighlightAction(), ChatSnippet::getDirection(message)); + return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, message.getHighlightActionSender(), ChatSnippet::getDirection(message)); } QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const std::string& background) { @@ -494,7 +494,7 @@ QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const s } QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) { - return getHighlightSpanStart(highlight.getTextColor(), highlight.getTextBackground()); + return getHighlightSpanStart(highlight.getFrontColor().get_value_or(""), highlight.getBackColor().get_value_or("")); } QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& message) { @@ -523,7 +523,7 @@ QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& messa continue; } if ((highlightPart = std::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part))) { - QString spanStart = getHighlightSpanStart(highlightPart->action.getTextColor(), highlightPart->action.getTextBackground()); + QString spanStart = getHighlightSpanStart(highlightPart->action.getFrontColor().get_value_or(""), highlightPart->action.getBackColor().get_value_or("")); result += spanStart + QtUtilities::htmlEscape(P2QSTRING(highlightPart->text)) + "</span>"; continue; } @@ -554,7 +554,7 @@ std::string QtWebKitChatView::addMessage( QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - bool highlightWholeMessage = highlight.highlightWholeMessage() && highlight.getTextBackground() != "" && highlight.getTextColor() != ""; + bool highlightWholeMessage = highlight.getFrontColor() || highlight.getBackColor(); QString highlightSpanStart = highlightWholeMessage ? getHighlightSpanStart(highlight) : ""; QString highlightSpanEnd = highlightWholeMessage ? "</span>" : ""; htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "</span>" ; @@ -572,7 +572,7 @@ std::string QtWebKitChatView::addMessage( } std::string QtWebKitChatView::addAction(const ChatWindow::ChatMessage& message, const std::string &senderName, bool senderIsSelf, std::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) { - return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, message.getFullMessageHighlightAction(), ChatSnippet::getDirection(message)); + return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, message.getHighlightActionSender(), ChatSnippet::getDirection(message)); } static QString encodeButtonArgument(const QString& str) { @@ -835,11 +835,11 @@ std::string QtWebKitChatView::addSystemMessage(const ChatWindow::ChatMessage& me } void QtWebKitChatView::replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) { - replaceMessage(" *" + chatMessageToHTML(message) + "*", id, time, "font-style:italic ", message.getFullMessageHighlightAction()); + replaceMessage(" *" + chatMessageToHTML(message) + "*", id, time, "font-style:italic ", message.getHighlightActionSender()); } void QtWebKitChatView::replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time) { - replaceMessage(chatMessageToHTML(message), id, time, "", message.getFullMessageHighlightAction()); + replaceMessage(chatMessageToHTML(message), id, time, "", message.getHighlightActionSender()); } void QtWebKitChatView::replaceSystemMessage(const ChatWindow::ChatMessage& message, const std::string& id, ChatWindow::TimestampBehaviour timestampBehavior) { @@ -876,8 +876,8 @@ void QtWebKitChatView::replaceMessage(const QString& message, const std::string& QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - QString highlightSpanStart = highlight.highlightWholeMessage() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightWholeMessage() ? "</span>" : ""; + QString highlightSpanStart = (highlight.getFrontColor() || highlight.getBackColor()) ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = (highlight.getFrontColor() || highlight.getBackColor()) ? "</span>" : ""; messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd; replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time)); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 3512120..69e99e7 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -145,9 +145,9 @@ sources = [ "QtContactEditWindow.cpp", "QtContactEditWidget.cpp", "QtSingleWindow.cpp", - "QtHighlightEditor.cpp", "QtColorToolButton.cpp", "QtClosableLineEdit.cpp", + "QtHighlightNotificationConfigDialog.cpp", "ChatSnippet.cpp", "MessageSnippet.cpp", "SystemMessageSnippet.cpp", @@ -216,7 +216,10 @@ sources = [ "QtUpdateFeedSelectionDialog.cpp", "Trellis/QtDynamicGridLayout.cpp", "Trellis/QtDNDTabBar.cpp", - "Trellis/QtGridSelectionDialog.cpp" + "Trellis/QtGridSelectionDialog.cpp", + "QtCheckBoxStyledItemDelegate.cpp", + "QtColorSelectionStyledItemDelegate.cpp", + "QtSoundSelectionStyledItemDelegate.cpp" ] if env["PLATFORM"] == "win32" : @@ -311,7 +314,7 @@ myenv.Uic4("QtAffiliationEditor.ui") myenv.Uic4("QtJoinMUCWindow.ui") myenv.Uic4("QtHistoryWindow.ui") myenv.Uic4("QtConnectionSettings.ui") -myenv.Uic4("QtHighlightEditor.ui") +myenv.Uic4("QtHighlightNotificationConfigDialog.ui") myenv.Uic4("QtBlockListEditorWindow.ui") myenv.Uic4("QtSpellCheckerWindow.ui") myenv.Uic4("QtUpdateFeedSelectionDialog.ui") |