diff options
| -rw-r--r-- | Swift/Controllers/Chat/ChatControllerBase.cpp | 4 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/ChatMessageParser.cpp | 9 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/ChatMessageParser.h | 4 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp | 36 | ||||
| -rw-r--r-- | Swift/Controllers/HighlightAction.cpp | 2 | ||||
| -rw-r--r-- | Swift/Controllers/HighlightAction.h | 11 | ||||
| -rw-r--r-- | Swift/Controllers/HighlightRule.cpp | 12 | ||||
| -rw-r--r-- | Swift/Controllers/HighlightRule.h | 2 | ||||
| -rw-r--r-- | Swift/Controllers/Highlighter.h | 1 | ||||
| -rw-r--r-- | Swift/QtUI/QtHighlightEditor.cpp | 45 | ||||
| -rw-r--r-- | Swift/QtUI/QtHighlightEditor.ui | 7 | ||||
| -rw-r--r-- | Swift/QtUI/QtWebKitChatView.cpp | 8 |
12 files changed, 88 insertions, 53 deletions
diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 24341e6..519deda 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -1,373 +1,373 @@ /* * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <sstream> #include <map> #include <boost/bind.hpp> #include <boost/shared_ptr.hpp> #include <boost/smart_ptr/make_shared.hpp> #include <boost/date_time/posix_time/posix_time.hpp> #include <boost/numeric/conversion/cast.hpp> #include <boost/algorithm/string.hpp> #include <Swiften/Base/format.h> #include <Swiften/Base/Path.h> #include <Swiften/Base/String.h> #include <Swiften/Client/StanzaChannel.h> #include <Swiften/Elements/Delay.h> #include <Swiften/Elements/MUCInvitationPayload.h> #include <Swiften/Elements/MUCUserPayload.h> #include <Swiften/Base/foreach.h> #include <Swiften/Disco/EntityCapsProvider.h> #include <Swiften/Queries/Requests/GetSecurityLabelsCatalogRequest.h> #include <Swiften/Avatars/AvatarManager.h> #include <Swift/Controllers/Intl.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> #include <Swift/Controllers/HighlightManager.h> #include <Swift/Controllers/Highlighter.h> #include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/Chat/ChatMessageParser.h> namespace Swift { ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); chatWindow_->onSendMessageRequest.connect(boost::bind(&ChatControllerBase::handleSendMessageRequest, this, _1, _2)); chatWindow_->onLogCleared.connect(boost::bind(&ChatControllerBase::handleLogCleared, this)); entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1)); highlighter_ = highlightManager->createHighlighter(); setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable()); createDayChangeTimer(); } ChatControllerBase::~ChatControllerBase() { delete chatWindow_; } void ChatControllerBase::handleLogCleared() { cancelReplaces(); } ChatWindow* ChatControllerBase::detachChatWindow() { ChatWindow* chatWindow = chatWindow_; chatWindow_ = NULL; return chatWindow; } void ChatControllerBase::handleCapsChanged(const JID& jid) { if (jid.compare(toJID_, JID::WithoutResource) == 0) { handleBareJIDCapsChanged(jid); } } void ChatControllerBase::setCanStartImpromptuChats(bool supportsImpromptu) { if (chatWindow_) { chatWindow_->setCanInitiateImpromptuChats(supportsImpromptu); } } void ChatControllerBase::createDayChangeTimer() { if (timerFactory_) { boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); boost::posix_time::ptime midnight(now.date() + boost::gregorian::days(1)); int millisecondsUntilMidnight = boost::numeric_cast<int>((midnight - now).total_milliseconds()); dateChangeTimer_ = boost::shared_ptr<Timer>(timerFactory_->createTimer(millisecondsUntilMidnight)); dateChangeTimer_->onTick.connect(boost::bind(&ChatControllerBase::handleDayChangeTick, this)); dateChangeTimer_->start(); } } void ChatControllerBase::handleDayChangeTick() { dateChangeTimer_->stop(); boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "The day is now %1%")) % std::string(boost::posix_time::to_iso_extended_string(now)).substr(0,10))), ChatWindow::DefaultDirection); dayTicked(); createDayChangeTimer(); } void ChatControllerBase::setEnabled(bool enabled) { chatWindow_->setInputEnabled(enabled); } void ChatControllerBase::setOnline(bool online) { setEnabled(online); } JID ChatControllerBase::getBaseJID() { return JID(toJID_.toBare()); } void ChatControllerBase::setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info) { if (iqRouter_->isAvailable() && info->hasFeature(DiscoInfo::SecurityLabelsCatalogFeature)) { GetSecurityLabelsCatalogRequest::ref request = GetSecurityLabelsCatalogRequest::create(getBaseJID(), iqRouter_); request->onResponse.connect(boost::bind(&ChatControllerBase::handleSecurityLabelsCatalogResponse, this, _1, _2)); request->send(); } else { chatWindow_->setSecurityLabelsEnabled(false); labelsEnabled_ = false; } } void ChatControllerBase::handleAllMessagesRead() { if (!unreadMessages_.empty()) { targetedUnreadMessages_.clear(); foreach (boost::shared_ptr<StanzaEvent> stanzaEvent, unreadMessages_) { stanzaEvent->conclude(); } unreadMessages_.clear(); chatWindow_->setUnreadMessageCount(0); onUnreadCountChanged(); } } int ChatControllerBase::getUnreadCount() { return boost::numeric_cast<int>(targetedUnreadMessages_.size()); } void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool isCorrectionMessage) { if (!stanzaChannel_->isAvailable() || body.empty()) { return; } boost::shared_ptr<Message> message(new Message()); message->setTo(toJID_); message->setType(Swift::Message::Chat); message->setBody(body); if (labelsEnabled_) { if (!isCorrectionMessage) { lastLabel_ = chatWindow_->getSelectedSecurityLabel(); } SecurityLabelsCatalog::Item labelItem = lastLabel_; if (labelItem.getLabel()) { message->addPayload(labelItem.getLabel()); } } preSendMessageRequest(message); boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); if (useDelayForLatency_) { message->addPayload(boost::make_shared<Delay>(now, selfJID_)); } if (isCorrectionMessage) { message->addPayload(boost::shared_ptr<Replace> (new Replace(lastSentMessageStanzaID_))); } message->setID(lastSentMessageStanzaID_ = idGenerator_.generateID()); stanzaChannel_->sendMessage(message); postSendMessage(message->getBody(), boost::dynamic_pointer_cast<Stanza>(message)); onActivity(message->getBody()); #ifdef SWIFT_EXPERIMENTAL_HISTORY logMessage(body, selfJID_, toJID_, now, false); #endif } void ChatControllerBase::handleSecurityLabelsCatalogResponse(boost::shared_ptr<SecurityLabelsCatalog> catalog, ErrorPayload::ref error) { if (catalog && !error) { if (catalog->getItems().size() == 0) { chatWindow_->setSecurityLabelsEnabled(false); labelsEnabled_ = false; } else { labelsEnabled_ = true; chatWindow_->setAvailableSecurityLabels(catalog->getItems()); chatWindow_->setSecurityLabelsEnabled(true); } } else { labelsEnabled_ = false; chatWindow_->setSecurityLabelsError(); } } void ChatControllerBase::showChatWindow() { chatWindow_->show(); } void ChatControllerBase::activateChatWindow() { chatWindow_->activate(); } bool ChatControllerBase::hasOpenWindow() const { return chatWindow_ && chatWindow_->isVisible(); } std::string ChatControllerBase::addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const boost::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) { if (boost::starts_with(message, "/me ")) { return chatWindow_->addAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight); } else { - return chatWindow_->addMessage(chatMessageParser_->parseMessageBody(message,senderIsSelf), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight); + return chatWindow_->addMessage(chatMessageParser_->parseMessageBody(message,highlighter_->getNick(),senderIsSelf), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight); } } void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, bool senderIsSelf, const boost::posix_time::ptime& time, const HighlightAction& highlight) { if (boost::starts_with(message, "/me ")) { chatWindow_->replaceWithAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), id, time, highlight); } else { - chatWindow_->replaceMessage(chatMessageParser_->parseMessageBody(message,senderIsSelf), id, time, highlight); + chatWindow_->replaceMessage(chatMessageParser_->parseMessageBody(message,highlighter_->getNick(),senderIsSelf), id, time, highlight); } } bool ChatControllerBase::isFromContact(const JID& from) { return from.toBare() == toJID_.toBare(); } void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) { preHandleIncomingMessage(messageEvent); if (messageEvent->isReadable() && !messageEvent->getConcluded()) { unreadMessages_.push_back(messageEvent); if (messageEvent->targetsMe()) { targetedUnreadMessages_.push_back(messageEvent); } } boost::shared_ptr<Message> message = messageEvent->getStanza(); std::string body = message->getBody(); HighlightAction highlight; if (message->isError()) { if (!message->getTo().getResource().empty()) { std::string errorMessage = str(format(QT_TRANSLATE_NOOP("", "Couldn't send message: %1%")) % getErrorMessage(message->getPayload<ErrorPayload>())); chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); } } else if (messageEvent->getStanza()->getPayload<MUCInvitationPayload>()) { handleMUCInvitation(messageEvent->getStanza()); return; } else if (messageEvent->getStanza()->getPayload<MUCUserPayload>() && messageEvent->getStanza()->getPayload<MUCUserPayload>()->getInvite()) { handleMediatedMUCInvitation(messageEvent->getStanza()); return; } else { if (!messageEvent->isReadable()) { return; } showChatWindow(); JID from = message->getFrom(); std::vector<boost::shared_ptr<Delay> > delayPayloads = message->getPayloads<Delay>(); for (size_t i = 0; useDelayForLatency_ && i < delayPayloads.size(); i++) { if (!delayPayloads[i]->getFrom()) { continue; } boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); std::ostringstream s; s << "The following message took " << (now - delayPayloads[i]->getStamp()).total_milliseconds() / 1000.0 << " seconds to be delivered from " << delayPayloads[i]->getFrom()->toString() << "."; chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(std::string(s.str())), ChatWindow::DefaultDirection); } boost::shared_ptr<SecurityLabel> label = message->getPayload<SecurityLabel>(); // Determine the timestamp boost::posix_time::ptime timeStamp = boost::posix_time::microsec_clock::universal_time(); boost::optional<boost::posix_time::ptime> messageTimeStamp = getMessageTimestamp(message); if (messageTimeStamp) { timeStamp = *messageTimeStamp; } onActivity(body); // Highlight if (!isIncomingMessageFromMe(message)) { highlight = highlighter_->findAction(body, senderDisplayNameFromMessage(from)); } boost::shared_ptr<Replace> replace = message->getPayload<Replace>(); if (replace) { std::string body = message->getBody(); // Should check if the user has a previous message std::map<JID, std::string>::iterator lastMessage; lastMessage = lastMessagesUIID_.find(from); if (lastMessage != lastMessagesUIID_.end()) { replaceMessage(body, lastMessagesUIID_[from], isIncomingMessageFromMe(message), timeStamp, highlight); } } else { lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, avatarManager_->getAvatarPath(from), timeStamp, highlight); } logMessage(body, from, selfJID_, timeStamp, true); } chatWindow_->show(); chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); postHandleIncomingMessage(messageEvent, highlight); } std::string ChatControllerBase::getErrorMessage(boost::shared_ptr<ErrorPayload> error) { std::string defaultMessage = QT_TRANSLATE_NOOP("", "Error sending message"); if (!error->getText().empty()) { return error->getText(); } else { switch (error->getCondition()) { case ErrorPayload::BadRequest: return QT_TRANSLATE_NOOP("", "Bad request"); case ErrorPayload::Conflict: return QT_TRANSLATE_NOOP("", "Conflict"); case ErrorPayload::FeatureNotImplemented: return QT_TRANSLATE_NOOP("", "This feature is not implemented"); case ErrorPayload::Forbidden: return QT_TRANSLATE_NOOP("", "Forbidden"); case ErrorPayload::Gone: return QT_TRANSLATE_NOOP("", "Recipient can no longer be contacted"); case ErrorPayload::InternalServerError: return QT_TRANSLATE_NOOP("", "Internal server error"); case ErrorPayload::ItemNotFound: return QT_TRANSLATE_NOOP("", "Item not found"); case ErrorPayload::JIDMalformed: return QT_TRANSLATE_NOOP("", "JID Malformed"); case ErrorPayload::NotAcceptable: return QT_TRANSLATE_NOOP("", "Message was rejected"); case ErrorPayload::NotAllowed: return QT_TRANSLATE_NOOP("", "Not allowed"); case ErrorPayload::NotAuthorized: return QT_TRANSLATE_NOOP("", "Not authorized"); case ErrorPayload::PaymentRequired: return QT_TRANSLATE_NOOP("", "Payment is required"); case ErrorPayload::RecipientUnavailable: return QT_TRANSLATE_NOOP("", "Recipient is unavailable"); case ErrorPayload::Redirect: return QT_TRANSLATE_NOOP("", "Redirect"); case ErrorPayload::RegistrationRequired: return QT_TRANSLATE_NOOP("", "Registration required"); case ErrorPayload::RemoteServerNotFound: return QT_TRANSLATE_NOOP("", "Recipient's server not found"); case ErrorPayload::RemoteServerTimeout: return QT_TRANSLATE_NOOP("", "Remote server timeout"); case ErrorPayload::ResourceConstraint: return QT_TRANSLATE_NOOP("", "The server is low on resources"); case ErrorPayload::ServiceUnavailable: return QT_TRANSLATE_NOOP("", "The service is unavailable"); case ErrorPayload::SubscriptionRequired: return QT_TRANSLATE_NOOP("", "A subscription is required"); case ErrorPayload::UndefinedCondition: return QT_TRANSLATE_NOOP("", "Undefined condition"); case ErrorPayload::UnexpectedRequest: return QT_TRANSLATE_NOOP("", "Unexpected request"); } } assert(false); return defaultMessage; } void ChatControllerBase::handleGeneralMUCInvitation(MUCInviteEvent::ref event) { unreadMessages_.push_back(event); chatWindow_->show(); chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); chatWindow_->addMUCInvitation(senderDisplayNameFromMessage(event->getInviter()), event->getRoomJID(), event->getReason(), event->getPassword(), event->getDirect(), event->getImpromptu()); eventController_->handleIncomingEvent(event); } void ChatControllerBase::handleMUCInvitation(Message::ref message) { MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); if (autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { eventStream_->send(boost::make_shared<JoinMUCUIEvent>(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true)); } else { MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true, invite->getIsImpromptu()); handleGeneralMUCInvitation(inviteEvent); } } void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { MUCUserPayload::Invite invite = *message->getPayload<MUCUserPayload>()->getInvite(); JID from = message->getFrom(); std::string reason; if (!invite.reason.empty()) { reason = invite.reason; } std::string password; if (message->getPayload<MUCUserPayload>()->getPassword()) { password = *message->getPayload<MUCUserPayload>()->getPassword(); } MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(invite.from, from, reason, password, false, false); handleGeneralMUCInvitation(inviteEvent); } } diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp index 09d93ac..5a608db 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.cpp +++ b/Swift/Controllers/Chat/ChatMessageParser.cpp @@ -1,194 +1,195 @@ /* * Copyright (c) 2013-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #include <Swift/Controllers/Chat/ChatMessageParser.h> #include <vector> #include <utility> #include <boost/smart_ptr/make_shared.hpp> #include <boost/algorithm/string.hpp> #include <Swiften/Base/Regex.h> #include <Swiften/Base/foreach.h> #include <SwifTools/Linkify.h> namespace Swift { ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode) : emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) { } typedef std::pair<std::string, std::string> StringPair; - ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, bool senderIsSelf) { + ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& nick, bool senderIsSelf) { ChatWindow::ChatMessage parsedMessage; std::string remaining = body; /* Parse one, URLs */ while (!remaining.empty()) { bool found = false; std::pair<std::vector<std::string>, size_t> links = Linkify::splitLink(remaining); remaining = ""; for (size_t i = 0; i < links.first.size(); i++) { const std::string& part = links.first[i]; if (found) { // Must be on the last part, then remaining = part; } else { if (i == links.second) { found = true; parsedMessage.append(boost::make_shared<ChatWindow::ChatURIMessagePart>(part)); } else { parsedMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(part)); } } } } /* do emoticon substitution */ parsedMessage = emoticonHighlight(parsedMessage); if (!senderIsSelf) { /* do not highlight our own messsages */ /* do word-based color highlighting */ - parsedMessage = splitHighlight(parsedMessage); + parsedMessage = splitHighlight(parsedMessage, nick); } return parsedMessage; } ChatWindow::ChatMessage ChatMessageParser::emoticonHighlight(const ChatWindow::ChatMessage& message) { ChatWindow::ChatMessage parsedMessage = message; std::string regexString; /* Parse two, emoticons */ foreach (StringPair emoticon, emoticons_) { /* Construct a regexp that finds an instance of any of the emoticons inside a group * at the start or end of the line, or beside whitespace. */ regexString += regexString.empty() ? "" : "|"; std::string escaped = "(" + Regex::escape(emoticon.first) + ")"; regexString += "^" + escaped + "|"; regexString += escaped + "$|"; regexString += "\\s" + escaped + "|"; regexString += escaped + "\\s"; } if (!regexString.empty()) { regexString += ""; boost::regex emoticonRegex(regexString); ChatWindow::ChatMessage newMessage; foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, parsedMessage.getParts()) { boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { try { boost::match_results<std::string::const_iterator> match; const std::string& text = textPart->text; std::string::const_iterator start = text.begin(); while (regex_search(start, text.end(), match, emoticonRegex)) { int matchIndex = 0; for (matchIndex = 1; matchIndex < static_cast<int>(match.size()); matchIndex++) { if (match[matchIndex].length() > 0) { //This is the matching subgroup break; } } std::string::const_iterator matchStart = match[matchIndex].first; std::string::const_iterator matchEnd = match[matchIndex].second; if (start != matchStart) { /* If we're skipping over plain text since the previous emoticon, record it as plain text */ newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); } boost::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart = boost::make_shared<ChatWindow::ChatEmoticonMessagePart>(); std::string matchString = match[matchIndex].str(); std::map<std::string, std::string>::const_iterator emoticonIterator = emoticons_.find(matchString); assert (emoticonIterator != emoticons_.end()); const StringPair& emoticon = *emoticonIterator; emoticonPart->imagePath = emoticon.second; emoticonPart->alternativeText = emoticon.first; newMessage.append(emoticonPart); start = matchEnd; } if (start != text.end()) { /* If there's plain text after the last emoticon, record it */ newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); } } catch (std::runtime_error) { /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ newMessage.append(part); } } else { newMessage.append(part); } } parsedMessage = newMessage; } return parsedMessage; } - ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message) + 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 */ } - foreach(const boost::regex ®ex, rule.getKeywordRegex()) { + const std::vector<boost::regex> keywordRegex = rule.getKeywordRegex(nick); + foreach(const boost::regex& regex, keywordRegex) { ChatWindow::ChatMessage newMessage; foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, parsedMessage.getParts()) { boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { try { boost::match_results<std::string::const_iterator> match; const std::string& text = textPart->text; std::string::const_iterator start = text.begin(); while (regex_search(start, text.end(), match, regex)) { std::string::const_iterator matchStart = match[0].first; std::string::const_iterator matchEnd = match[0].second; if (start != matchStart) { /* If we're skipping over plain text since the previous emoticon, record it as plain text */ newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); } boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = boost::make_shared<ChatWindow::ChatHighlightingMessagePart>(); highlightPart->text = match.str(); highlightPart->foregroundColor = rule.getAction().getTextColor(); highlightPart->backgroundColor = rule.getAction().getTextBackground(); newMessage.append(highlightPart); start = matchEnd; } if (start != text.end()) { /* If there's plain text after the last emoticon, record it */ newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); } } catch (std::runtime_error) { /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ newMessage.append(part); } } else { newMessage.append(part); } } parsedMessage = newMessage; } } return parsedMessage; } } diff --git a/Swift/Controllers/Chat/ChatMessageParser.h b/Swift/Controllers/Chat/ChatMessageParser.h index cff4ffa..2f5c171 100644 --- a/Swift/Controllers/Chat/ChatMessageParser.h +++ b/Swift/Controllers/Chat/ChatMessageParser.h @@ -1,26 +1,26 @@ /* * Copyright (c) 2013-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #pragma once #include <string> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> namespace Swift { class ChatMessageParser { public: ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode = false); - ChatWindow::ChatMessage parseMessageBody(const std::string& body, bool senderIsSelf = false); + ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& nick = "", bool senderIsSelf = false); private: ChatWindow::ChatMessage emoticonHighlight(const ChatWindow::ChatMessage& parsedMessage); - ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage); + ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& nick); std::map<std::string, std::string> emoticons_; HighlightRulesListPtr highlightRules_; bool mucMode_; }; } diff --git a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp index 5dca63a..2a07654 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp @@ -1,231 +1,267 @@ /* * Copyright (c) 2013-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <hippomocks.h> #include <Swift/Controllers/Chat/ChatMessageParser.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_SUITE_END(); public: void setUp() { smile1_ = ":)"; smile1Path_ = "/blah/smile1.png"; smile2_ = ":("; smile2Path_ = "/blah/smile2.jpg"; emoticons_[smile1_] = smile1Path_; emoticons_[smile2_] = smile2Path_; } void tearDown() { emoticons_.clear(); } void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { boost::shared_ptr<ChatWindow::ChatTextMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(result.getParts()[index]); CPPUNIT_ASSERT_EQUAL(text, part->text); } void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { boost::shared_ptr<ChatWindow::ChatEmoticonMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(result.getParts()[index]); CPPUNIT_ASSERT(!!part); CPPUNIT_ASSERT_EQUAL(text, part->alternativeText); CPPUNIT_ASSERT_EQUAL(path, part->imagePath); } void assertHighlight(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(result.getParts()[index]); CPPUNIT_ASSERT_EQUAL(text, part->text); } void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { boost::shared_ptr<ChatWindow::ChatURIMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(result.getParts()[index]); CPPUNIT_ASSERT_EQUAL(text, part->target); } static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) { HighlightRule rule; std::vector<std::string> keywords; keywords.push_back(keyword); rule.setKeywords(keywords); rule.setMatchCase(matchCase); rule.setMatchWholeWords(matchWholeWord); rule.setMatchChat(true); return rule; } static const HighlightRulesListPtr ruleListFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) { boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>(); list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord)); return list; } static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2) { boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>(); list->addRule(rule1); list->addRule(rule2); return list; } + static HighlightRulesListPtr ruleListWithNickHighlight() + { + HighlightRule rule; + rule.setMatchChat(true); + rule.setNickIsKeyword(true); + rule.setMatchCase(true); + rule.setMatchWholeWords(true); + boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::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_, boost::make_shared<HighlightManager::HighlightRulesList>()); ChatWindow::ChatMessage result = testling.parseMessageBody(no_special_message); assertText(result, 0, no_special_message); testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); assertEmoticon(result, 0, smile1_, smile1Path_); assertText(result, 1, " shiny "); assertEmoticon(result, 2, smile2_, smile2Path_); assertText(result, 3, " "); assertHighlight(result, 4, "trigger"); 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"); assertText(result, 2, "message"); testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, true)); result = testling.parseMessageBody("testtriggermessage"); assertText(result, 0, "testtriggermessage"); testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", true, false)); result = testling.parseMessageBody("TrIgGeR"); assertText(result, 0, "TrIgGeR"); testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); result = testling.parseMessageBody("TrIgGeR"); assertHighlight(result, 0, "TrIgGeR"); testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); result = testling.parseMessageBody("partialTrIgGeRmatch"); assertText(result, 0, "partial"); assertHighlight(result, 1, "TrIgGeR"); assertText(result, 2, "match"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); result = testling.parseMessageBody("zero one two three"); assertText(result, 0, "zero "); assertHighlight(result, 1, "one"); assertText(result, 2, " two "); assertHighlight(result, 3, "three"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); result = testling.parseMessageBody("zero oNe two tHrEe"); assertText(result, 0, "zero "); assertHighlight(result, 1, "oNe"); assertText(result, 2, " two "); assertHighlight(result, 3, "tHrEe"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", true, false))); result = testling.parseMessageBody("zero oNe two tHrEe"); assertText(result, 0, "zero "); assertHighlight(result, 1, "oNe"); assertText(result, 2, " two tHrEe"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", true, false))); result = testling.parseMessageBody("zero oNe two tHrEe"); assertText(result, 0, "zero oNe two tHrEe"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); result = testling.parseMessageBody("zeroonetwothree"); assertText(result, 0, "zero"); assertHighlight(result, 1, "one"); assertText(result, 2, "two"); assertHighlight(result, 3, "three"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", false, false))); result = testling.parseMessageBody("zeroOnEtwoThReE"); assertText(result, 0, "zeroOnEtwo"); assertHighlight(result, 1, "ThReE"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, false))); result = testling.parseMessageBody("zeroonetwothree"); assertText(result, 0, "zeroonetwo"); assertHighlight(result, 1, "three"); testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, true))); result = testling.parseMessageBody("zeroonetwothree"); assertText(result, 0, "zeroonetwothree"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Alice", "Alice"); + assertHighlight(result, 0, "Alice"); + + 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"); + assertText(result, 2, " Text"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Alice Text", "Alice"); + assertHighlight(result, 0, "Alice"); + assertText(result, 1, " Text"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Text Alice", "Alice"); + assertText(result, 0, "Text "); + assertHighlight(result, 1, "Alice"); } void testOneEmoticon() { ChatMessageParser testling(emoticons_, boost::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_, boost::make_shared<HighlightManager::HighlightRulesList>()); ChatWindow::ChatMessage result = testling.parseMessageBody(":)"); assertEmoticon(result, 0, smile1_, smile1Path_); } void testHiddenEmoticon() { ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a"); assertText(result, 0, "b:)a"); } void testEndlineEmoticon() { ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); ChatWindow::ChatMessage result = testling.parseMessageBody("Lazy:)"); assertText(result, 0, "Lazy"); assertEmoticon(result, 1, smile1_, smile1Path_); } void testBoundedEmoticons() { ChatMessageParser testling(emoticons_, boost::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 testEmoticonParenthesis() { ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); ChatWindow::ChatMessage result = testling.parseMessageBody("(Like this :))"); assertText(result, 0, "(Like this "); assertEmoticon(result, 1, smile1_, smile1Path_); assertText(result, 2, ")"); } private: std::map<std::string, std::string> emoticons_; std::string smile1_; std::string smile1Path_; std::string smile2_; std::string smile2Path_; }; CPPUNIT_TEST_SUITE_REGISTRATION(ChatMessageParserTest); diff --git a/Swift/Controllers/HighlightAction.cpp b/Swift/Controllers/HighlightAction.cpp index d4d199d..492d4d2 100644 --- a/Swift/Controllers/HighlightAction.cpp +++ b/Swift/Controllers/HighlightAction.cpp @@ -1,28 +1,28 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ #include <Swift/Controllers/HighlightAction.h> namespace Swift { -void HighlightAction::setHighlightText(bool highlightText) +void HighlightAction::setHighlightAllText(bool highlightText) { highlightText_ = highlightText; if (!highlightText_) { textColor_.clear(); textBackground_.clear(); } } void HighlightAction::setPlaySound(bool playSound) { playSound_ = playSound; if (!playSound_) { soundFile_.clear(); } } } diff --git a/Swift/Controllers/HighlightAction.h b/Swift/Controllers/HighlightAction.h index de1f201..90768a7 100644 --- a/Swift/Controllers/HighlightAction.h +++ b/Swift/Controllers/HighlightAction.h @@ -1,76 +1,79 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ /* * Copyright (c) 2014 Kevin Smith and Remko Tronçon * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #pragma once #include <string> #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp> namespace Swift { class HighlightRule; class HighlightAction { public: HighlightAction() : highlightText_(false), playSound_(false) {} - bool highlightText() const { return highlightText_; } - void setHighlightText(bool highlightText); + /** + * Gets the flag that indicates the entire message should be highlighted. + */ + bool highlightAllText() const { return highlightText_; } + void setHighlightAllText(bool highlightText); /** - * Gets the foreground highlight color. If the string is empty, assume a default color. + * 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. If the string is empty, assume a default color. + * 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 !highlightText_ && !playSound_; } private: friend class boost::serialization::access; template<class Archive> void serialize(Archive & ar, const unsigned int version); bool highlightText_; std::string textColor_; std::string textBackground_; bool playSound_; std::string soundFile_; }; template<class Archive> void HighlightAction::serialize(Archive& ar, const unsigned int /*version*/) { ar & highlightText_; ar & textColor_; ar & textBackground_; ar & playSound_; ar & soundFile_; } } diff --git a/Swift/Controllers/HighlightRule.cpp b/Swift/Controllers/HighlightRule.cpp index f1a5235..3251067 100644 --- a/Swift/Controllers/HighlightRule.cpp +++ b/Swift/Controllers/HighlightRule.cpp @@ -1,140 +1,152 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ /* * Copyright (c) 2014 Kevin Smith and Remko Tronçon * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #include <algorithm> #include <boost/algorithm/string.hpp> #include <boost/lambda/lambda.hpp> #include <Swiften/Base/foreach.h> #include <Swiften/Base/Regex.h> #include <Swift/Controllers/HighlightRule.h> namespace Swift { HighlightRule::HighlightRule() : nickIsKeyword_(false) , matchCase_(false) , matchWholeWords_(false) , matchChat_(false) , matchMUC_(false) { } boost::regex HighlightRule::regexFromString(const std::string & s) const { 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(); foreach (const std::string & k, keywords_) { keywordRegex_.push_back(regexFromString(k)); } senderRegex_.clear(); foreach (const std::string & s, senders_) { senderRegex_.push_back(regexFromString(s)); } } std::string HighlightRule::boolToString(bool b) { return b ? "1" : "0"; } bool HighlightRule::boolFromString(const std::string& s) { return s == "1"; } 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 && nickIsKeyword_ && !nick.empty()) { if (boost::regex_search(body, regexFromString(nick))) { matchesKeyword = true; } } foreach (const boost::regex & rx, senderRegex_) { if (boost::regex_search(sender, rx)) { matchesSender = true; break; } } if (matchesKeyword && matchesSender) { return true; } } return false; } void HighlightRule::setSenders(const std::vector<std::string>& senders) { senders_ = senders; updateRegex(); } void HighlightRule::setKeywords(const std::vector<std::string>& keywords) { keywords_ = keywords; updateRegex(); } +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(); } } diff --git a/Swift/Controllers/HighlightRule.h b/Swift/Controllers/HighlightRule.h index ae1a3d3..c0226bc 100644 --- a/Swift/Controllers/HighlightRule.h +++ b/Swift/Controllers/HighlightRule.h @@ -1,101 +1,101 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ /* * Copyright (c) 2014 Kevin Smith and Remko Tronçon * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #pragma once #include <vector> #include <string> #include <boost/regex.hpp> #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.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>&); - const std::vector<boost::regex>& getKeywordRegex() const { return keywordRegex_; } + 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_; }; 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.h b/Swift/Controllers/Highlighter.h index d026f29..d5d846b 100644 --- a/Swift/Controllers/Highlighter.h +++ b/Swift/Controllers/Highlighter.h @@ -1,37 +1,38 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ #pragma once #include <string> #include <Swift/Controllers/HighlightRule.h> namespace Swift { class HighlightManager; class Highlighter { public: Highlighter(HighlightManager* manager); enum Mode { ChatMode, MUCMode }; void setMode(Mode mode); void setNick(const std::string& nick) { nick_ = nick; } + std::string getNick() const { return nick_; } HighlightAction findAction(const std::string& body, const std::string& sender) const; void handleHighlightAction(const HighlightAction& action); private: HighlightManager* manager_; Mode mode_; HighlightRule::MessageType messageType_; std::string nick_; }; } diff --git a/Swift/QtUI/QtHighlightEditor.cpp b/Swift/QtUI/QtHighlightEditor.cpp index ce07003..8488d7d 100644 --- a/Swift/QtUI/QtHighlightEditor.cpp +++ b/Swift/QtUI/QtHighlightEditor.cpp @@ -1,524 +1,513 @@ /* * Copyright (c) 2012 Maciej Niedzielski * Licensed under the simplified BSD license. * See Documentation/Licenses/BSD-simplified.txt for more information. */ /* * Copyright (c) 2014 Kevin Smith and Remko Tronçon * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ #include <cassert> #include <boost/lexical_cast.hpp> #include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> #include <Swift/Controllers/HighlightManager.cpp> #include <Swift/QtUI/QtHighlightEditor.h> #include <Swift/QtUI/QtSwiftUtil.h> #include <Swift/QtUI/QtSettingsProvider.h> #include <QTreeWidgetItem> #include <QFileDialog> namespace Swift { QtHighlightEditor::QtHighlightEditor(QtSettingsProvider* settings, QWidget* parent) : QWidget(parent), settings_(settings), previousRow_(-1) { ui_.setupUi(this); connect(ui_.listWidget, SIGNAL(currentRowChanged(int)), SLOT(onCurrentRowChanged(int))); connect(ui_.newButton, SIGNAL(clicked()), SLOT(onNewButtonClicked())); connect(ui_.deleteButton, SIGNAL(clicked()), SLOT(onDeleteButtonClicked())); connect(ui_.buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(onApplyButtonClick())); connect(ui_.buttonBox->button(QDialogButtonBox::Cancel), SIGNAL(clicked()), SLOT(onCancelButtonClick())); connect(ui_.buttonBox->button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(onOkButtonClick())); connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect())); - connect(ui_.defaultColorRadio, SIGNAL(clicked()), SLOT(colorOtherSelect())); connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(colorCustomSelect())); connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(soundOtherSelect())); connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(soundCustomSelect())); /* replace the static line-edit control with the roster autocompleter */ ui_.dummySenderName->setVisible(false); jid_ = new QtSuggestingJIDInput(this, settings); ui_.senderName->addWidget(jid_); jid_->onUserSelected.connect(boost::bind(&QtHighlightEditor::handleOnUserSelected, this, _1)); /* handle autocomplete */ connect(jid_, SIGNAL(textEdited(QString)), SLOT(handleContactSuggestionRequested(QString))); /* we need to be notified if any of the state changes so that we can update our textual rule description */ connect(ui_.chatRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.roomRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.nickIsKeyword, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.allMsgRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.senderRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(jid_, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); connect(ui_.keywordRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.keyword, SIGNAL(textChanged(const QString&)), SLOT(widgetClick())); connect(ui_.matchPartialWords, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.matchCase, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.noColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); - connect(ui_.defaultColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.customColorRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.noSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.defaultSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); connect(ui_.customSoundRadio, SIGNAL(clicked()), SLOT(widgetClick())); /* allow selection of a custom sound file */ connect(ui_.soundFileButton, SIGNAL(clicked()), SLOT(selectSoundFile())); /* if these are not needed, then they should be removed */ ui_.moveUpButton->setVisible(false); ui_.moveDownButton->setVisible(false); setWindowTitle(tr("Highlight Rules")); } QtHighlightEditor::~QtHighlightEditor() { } std::string formatShortDescription(const HighlightRule &rule) { const std::string chatOrRoom = (rule.getMatchChat() ? "chat" : "room"); std::vector<std::string> senders = rule.getSenders(); std::vector<std::string> keywords = rule.getKeywords(); if (senders.empty() && keywords.empty() && !rule.getNickIsKeyword()) { return std::string("All ") + chatOrRoom + " messages."; } if (rule.getNickIsKeyword()) { return std::string("All ") + chatOrRoom + " messages that mention my name."; } if (!senders.empty()) { return std::string("All ") + chatOrRoom + " messages from " + senders[0] + "."; } if (!keywords.empty()) { return std::string("All ") + chatOrRoom + " messages mentioning the keyword '" + keywords[0] + "'."; } return "Unknown Rule"; } void QtHighlightEditor::show() { highlightManager_->loadSettings(); populateList(); if (ui_.listWidget->count()) { selectRow(0); } /* prepare default states */ widgetClick(); QWidget::show(); QWidget::activateWindow(); } void QtHighlightEditor::setHighlightManager(HighlightManager* highlightManager) { highlightManager_ = highlightManager; } void QtHighlightEditor::setContactSuggestions(const std::vector<Contact::ref>& suggestions) { jid_->setSuggestions(suggestions); } void QtHighlightEditor::colorOtherSelect() { ui_.foregroundColor->setEnabled(false); ui_.backgroundColor->setEnabled(false); } void QtHighlightEditor::colorCustomSelect() { ui_.foregroundColor->setEnabled(true); ui_.backgroundColor->setEnabled(true); } void QtHighlightEditor::soundOtherSelect() { ui_.soundFile->setEnabled(false); ui_.soundFileButton->setEnabled(false); } void QtHighlightEditor::soundCustomSelect() { ui_.soundFile->setEnabled(true); ui_.soundFileButton->setEnabled(true); } void QtHighlightEditor::onNewButtonClicked() { int row = getSelectedRow() + 1; populateList(); HighlightRule newRule; newRule.setMatchMUC(true); highlightManager_->insertRule(row, newRule); QListWidgetItem *item = new QListWidgetItem(); item->setText(P2QSTRING(formatShortDescription(newRule))); ui_.listWidget->insertItem(row, item); selectRow(row); } void QtHighlightEditor::onDeleteButtonClicked() { int selectedRow = getSelectedRow(); assert(selectedRow>=0 && selectedRow<ui_.listWidget->count()); delete ui_.listWidget->takeItem(selectedRow); highlightManager_->removeRule(selectedRow); if (!ui_.listWidget->count()) { disableDialog(); ui_.deleteButton->setEnabled(false); } else { if (selectedRow == ui_.listWidget->count()) { selectRow(ui_.listWidget->count() - 1); } else { selectRow(selectedRow); } } } void QtHighlightEditor::onCurrentRowChanged(int currentRow) { ui_.deleteButton->setEnabled(currentRow != -1); ui_.moveUpButton->setEnabled(currentRow != -1 && currentRow != 0); ui_.moveDownButton->setEnabled(currentRow != -1 && currentRow != (ui_.listWidget->count()-1)); if (previousRow_ != -1) { if (ui_.listWidget->count() > previousRow_) { highlightManager_->setRule(previousRow_, ruleFromDialog()); } } if (currentRow != -1) { HighlightRule rule = highlightManager_->getRule(currentRow); ruleToDialog(rule); if (ui_.listWidget->currentItem()) { ui_.listWidget->currentItem()->setText(P2QSTRING(formatShortDescription(rule))); } } /* grey the dialog if we have nothing selected */ if (currentRow == -1) { disableDialog(); } previousRow_ = currentRow; } void QtHighlightEditor::onApplyButtonClick() { selectRow(getSelectedRow()); /* force save */ highlightManager_->storeSettings(); } void QtHighlightEditor::onCancelButtonClick() { close(); } void QtHighlightEditor::onOkButtonClick() { onApplyButtonClick(); close(); } void QtHighlightEditor::setChildWidgetStates() { /* disable appropriate radio button child widgets */ if (ui_.chatRadio->isChecked()) { if (ui_.nickIsKeyword->isChecked()) { /* switch to another choice before we disable this button */ ui_.allMsgRadio->setChecked(true); } ui_.nickIsKeyword->setEnabled(false); } else if (ui_.roomRadio->isChecked()) { ui_.nickIsKeyword->setEnabled(true); } else { /* chats and rooms */ ui_.nickIsKeyword->setEnabled(true); } if (ui_.senderRadio->isChecked()) { jid_->setEnabled(true); } else { jid_->setEnabled(false); } if (ui_.keywordRadio->isChecked()) { ui_.keyword->setEnabled(true); ui_.matchPartialWords->setEnabled(true); ui_.matchCase->setEnabled(true); } else { ui_.keyword->setEnabled(false); ui_.matchPartialWords->setEnabled(false); ui_.matchCase->setEnabled(false); } if (ui_.chatRadio->isChecked()) { ui_.allMsgRadio->setText(tr("Apply to all chat messages")); } else { ui_.allMsgRadio->setText(tr("Apply to all room messages")); } } void QtHighlightEditor::widgetClick() { setChildWidgetStates(); HighlightRule rule = ruleFromDialog(); if (ui_.listWidget->currentItem()) { ui_.listWidget->currentItem()->setText(P2QSTRING(formatShortDescription(rule))); } } void QtHighlightEditor::disableDialog() { ui_.chatRadio->setEnabled(false); ui_.roomRadio->setEnabled(false); ui_.allMsgRadio->setEnabled(false); ui_.nickIsKeyword->setEnabled(false); ui_.senderRadio->setEnabled(false); ui_.dummySenderName->setEnabled(false); ui_.keywordRadio->setEnabled(false); ui_.keyword->setEnabled(false); ui_.matchPartialWords->setEnabled(false); ui_.matchCase->setEnabled(false); ui_.noColorRadio->setEnabled(false); - ui_.defaultColorRadio->setEnabled(false); ui_.customColorRadio->setEnabled(false); ui_.foregroundColor->setEnabled(false); ui_.backgroundColor->setEnabled(false); ui_.noSoundRadio->setEnabled(false); ui_.defaultSoundRadio->setEnabled(false); ui_.customSoundRadio->setEnabled(false); ui_.soundFile->setEnabled(false); ui_.soundFileButton->setEnabled(false); } void QtHighlightEditor::handleContactSuggestionRequested(const QString& text) { std::string stdText = Q2PSTRING(text); onContactSuggestionsRequested(stdText); } void QtHighlightEditor::selectSoundFile() { QString path = QFileDialog::getOpenFileName(this, tr("Select sound file..."), QString(), "Sounds (*.wav)"); ui_.soundFile->setText(path); } void QtHighlightEditor::handleOnUserSelected(const JID& jid) { /* this might seem like it should be standard behaviour for the suggesting input box, but is not desirable in all cases */ jid_->setText(P2QSTRING(jid.toString())); } void QtHighlightEditor::populateList() { previousRow_ = -1; ui_.listWidget->clear(); HighlightRulesListPtr rules = highlightManager_->getRules(); for (size_t i = 0; i < rules->getSize(); ++i) { const HighlightRule& rule = rules->getRule(i); QListWidgetItem *item = new QListWidgetItem(); item->setText(P2QSTRING(formatShortDescription(rule))); ui_.listWidget->addItem(item); } } void QtHighlightEditor::selectRow(int row) { for (int i = 0; i < ui_.listWidget->count(); ++i) { if (i == row) { ui_.listWidget->item(i)->setSelected(true); onCurrentRowChanged(i); } else { ui_.listWidget->item(i)->setSelected(false); } } ui_.listWidget->setCurrentRow(row); } int QtHighlightEditor::getSelectedRow() const { for (int i = 0; i < ui_.listWidget->count(); ++i) { if (ui_.listWidget->item(i)->isSelected()) { return i; } } return -1; } HighlightRule QtHighlightEditor::ruleFromDialog() { HighlightRule rule; if (ui_.chatRadio->isChecked()) { rule.setMatchChat(true); rule.setMatchMUC(false); } else { rule.setMatchChat(false); rule.setMatchMUC(true); } if (ui_.senderRadio->isChecked()) { QString senderName = jid_->text(); if (!senderName.isEmpty()) { std::vector<std::string> senders; senders.push_back(Q2PSTRING(senderName)); rule.setSenders(senders); } } if (ui_.keywordRadio->isChecked()) { QString keywordString = ui_.keyword->text(); if (!keywordString.isEmpty()) { std::vector<std::string> keywords; keywords.push_back(Q2PSTRING(keywordString)); rule.setKeywords(keywords); } } - rule.setNickIsKeyword(ui_.nickIsKeyword->isChecked()); - rule.setMatchWholeWords(!ui_.matchPartialWords->isChecked()); - rule.setMatchCase(ui_.matchCase->isChecked()); + if (ui_.nickIsKeyword->isChecked()) { + rule.setNickIsKeyword(true); + rule.setMatchWholeWords(true); + rule.setMatchCase(true); + } else { + rule.setMatchWholeWords(!ui_.matchPartialWords->isChecked()); + rule.setMatchCase(ui_.matchCase->isChecked()); + } HighlightAction& action = rule.getAction(); if (ui_.noColorRadio->isChecked()) { - action.setHighlightText(false); - action.setTextColor(""); - action.setTextBackground(""); - } else if (ui_.defaultColorRadio->isChecked()) { - action.setHighlightText(true); action.setTextColor(""); action.setTextBackground(""); } else { - action.setHighlightText(true); action.setTextColor(Q2PSTRING(ui_.foregroundColor->getColor().name())); action.setTextBackground(Q2PSTRING(ui_.backgroundColor->getColor().name())); } if (ui_.noSoundRadio->isChecked()) { action.setPlaySound(false); } else if (ui_.defaultSoundRadio->isChecked()) { action.setPlaySound(true); action.setSoundFile(""); } else { action.setPlaySound(true); action.setSoundFile(Q2PSTRING(ui_.soundFile->text())); } return rule; } void QtHighlightEditor::ruleToDialog(const HighlightRule& rule) { ui_.chatRadio->setEnabled(true); ui_.roomRadio->setEnabled(true); if (rule.getMatchMUC()) { ui_.chatRadio->setChecked(false); ui_.roomRadio->setChecked(true); } else { ui_.chatRadio->setChecked(true); ui_.roomRadio->setChecked(false); } ui_.allMsgRadio->setEnabled(true); ui_.allMsgRadio->setChecked(true); /* this is the default radio button */ jid_->setText(""); ui_.keyword->setText(""); ui_.matchPartialWords->setChecked(false); ui_.matchCase->setChecked(false); ui_.nickIsKeyword->setEnabled(true); if (rule.getNickIsKeyword()) { ui_.nickIsKeyword->setChecked(true); } ui_.senderRadio->setEnabled(true); std::vector<std::string> senders = rule.getSenders(); if (!senders.empty()) { ui_.senderRadio->setChecked(true); jid_->setText(P2QSTRING(senders[0])); } ui_.keywordRadio->setEnabled(true); std::vector<std::string> keywords = rule.getKeywords(); if (!keywords.empty()) { ui_.keywordRadio->setChecked(true); ui_.keyword->setText(P2QSTRING(keywords[0])); ui_.matchPartialWords->setChecked(!rule.getMatchWholeWords()); ui_.matchCase->setChecked(rule.getMatchCase()); } const HighlightAction& action = rule.getAction(); ui_.noColorRadio->setEnabled(true); - ui_.defaultColorRadio->setEnabled(true); ui_.customColorRadio->setEnabled(true); - if (action.highlightText()) { - if (action.getTextColor().empty() && action.getTextBackground().empty()) { - ui_.defaultColorRadio->setChecked(true); - ui_.foregroundColor->setEnabled(false); - ui_.backgroundColor->setEnabled(false); - } else { - ui_.foregroundColor->setEnabled(true); - ui_.backgroundColor->setEnabled(true); - QColor foregroundColor(P2QSTRING(action.getTextColor())); - ui_.foregroundColor->setColor(foregroundColor); - QColor backgroundColor(P2QSTRING(action.getTextBackground())); - ui_.backgroundColor->setColor(backgroundColor); - ui_.customColorRadio->setChecked(true); - } - } else { + 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(); } } diff --git a/Swift/QtUI/QtHighlightEditor.ui b/Swift/QtUI/QtHighlightEditor.ui index be2e99b..775771f 100644 --- a/Swift/QtUI/QtHighlightEditor.ui +++ b/Swift/QtUI/QtHighlightEditor.ui @@ -1,486 +1,479 @@ <?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="defaultColorRadio"> - <property name="text"> - <string>Default Color</string> - </property> - </widget> - </item> - <item> <widget class="QRadioButton" name="customColorRadio"> <property name="text"> <string>Custom Color</string> </property> </widget> </item> <item> <spacer name="horizontalSpacer_5"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="sizeHint" stdset="0"> <size> <width>40</width> <height>20</height> </size> </property> </spacer> </item> </layout> </item> <item> <layout class="QHBoxLayout" name="horizontalLayout_6"> <item> <spacer name="horizontalSpacer_2"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="sizeHint" stdset="0"> <size> <width>40</width> <height>20</height> </size> </property> </spacer> </item> <item> <widget class="Swift::QtColorToolButton" name="foregroundColor"> <property name="enabled"> <bool>false</bool> </property> <property name="text"> <string>&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> <spacer name="horizontalSpacer_4"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> <property name="sizeHint" stdset="0"> <size> <width>40</width> <height>20</height> </size> </property> </spacer> </item> <item> <widget class="QDialogButtonBox" name="buttonBox"> <property name="standardButtons"> <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> </property> </widget> </item> </layout> </item> </layout> </widget> <customwidgets> <customwidget> <class>Swift::QtColorToolButton</class> <extends>QToolButton</extends> <header>QtColorToolButton.h</header> </customwidget> </customwidgets> <resources/> <connections/> </ui> diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp index 1486293..a510e34 100644 --- a/Swift/QtUI/QtWebKitChatView.cpp +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -68,878 +68,878 @@ QtWebKitChatView::QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStr stack->setLineWidth(2); mainLayout->addWidget(stack); #else mainLayout->addWidget(webView_); #endif #ifdef SWIFT_EXPERIMENTAL_FT setAcceptDrops(true); #endif webPage_ = new QWebPage(this); webPage_->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); if (Log::getLogLevel() == Log::debug) { webPage_->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true); } webView_->setPage(webPage_); connect(webPage_, SIGNAL(selectionChanged()), SLOT(copySelectionToClipboard())); connect(webPage_, SIGNAL(scrollRequested(int, int, const QRect&)), SLOT(handleScrollRequested(int, int, const QRect&))); viewReady_ = false; isAtBottom_ = true; resetView(); jsBridge = new QtChatWindowJSBridge(); addToJSEnvironment("chatwindow", jsBridge); connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString,QString,QString))); } QtWebKitChatView::~QtWebKitChatView() { delete jsBridge; } void QtWebKitChatView::handleClearRequested() { QMessageBox messageBox(this); messageBox.setWindowTitle(tr("Clear log")); messageBox.setText(tr("You are about to clear the contents of your chat log.")); messageBox.setInformativeText(tr("Are you sure?")); messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); messageBox.setDefaultButton(QMessageBox::Yes); int button = messageBox.exec(); if (button == QMessageBox::Yes) { logCleared(); resetView(); } } void QtWebKitChatView::handleKeyPressEvent(QKeyEvent* event) { webView_->keyPressEvent(event); } void QtWebKitChatView::addMessageBottom(boost::shared_ptr<ChatSnippet> snippet) { if (viewReady_) { addToDOM(snippet); } else { /* If this asserts, the previous queuing code was necessary and should be reinstated */ assert(false); } } void QtWebKitChatView::addMessageTop(boost::shared_ptr<ChatSnippet> snippet) { // save scrollbar maximum value if (!topMessageAdded_) { scrollBarMaximum_ = webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); } topMessageAdded_ = true; QWebElement continuationElement = firstElement_.findFirst("#insert"); bool insert = snippet->getAppendToPrevious(); bool fallback = continuationElement.isNull(); boost::shared_ptr<ChatSnippet> newSnippet = (insert && fallback) ? snippet->getContinuationFallbackSnippet() : snippet; QWebElement newElement = snippetToDOM(newSnippet); if (insert && !fallback) { Q_ASSERT(!continuationElement.isNull()); continuationElement.replace(newElement); } else { continuationElement.removeFromDocument(); topInsertPoint_.prependOutside(newElement); } firstElement_ = newElement; if (lastElement_.isNull()) { lastElement_ = firstElement_; } if (fontSizeSteps_ != 0) { double size = 1.0 + 0.2 * fontSizeSteps_; QString sizeString(QString().setNum(size, 'g', 3) + "em"); const QWebElementCollection spans = firstElement_.findAll("span.swift_resizable"); Q_FOREACH (QWebElement span, spans) { span.setStyleProperty("font-size", sizeString); } } } QWebElement QtWebKitChatView::snippetToDOM(boost::shared_ptr<ChatSnippet> snippet) { QWebElement newElement = newInsertPoint_.clone(); newElement.setInnerXml(snippet->getContent()); Q_ASSERT(!newElement.isNull()); return newElement; } void QtWebKitChatView::addToDOM(boost::shared_ptr<ChatSnippet> snippet) { //qDebug() << snippet->getContent(); rememberScrolledToBottom(); bool insert = snippet->getAppendToPrevious(); QWebElement continuationElement = lastElement_.findFirst("#insert"); bool fallback = insert && continuationElement.isNull(); boost::shared_ptr<ChatSnippet> newSnippet = (insert && fallback) ? snippet->getContinuationFallbackSnippet() : snippet; QWebElement newElement = snippetToDOM(newSnippet); if (insert && !fallback) { Q_ASSERT(!continuationElement.isNull()); continuationElement.replace(newElement); } else { continuationElement.removeFromDocument(); newInsertPoint_.prependOutside(newElement); } lastElement_ = newElement; if (fontSizeSteps_ != 0) { double size = 1.0 + 0.2 * fontSizeSteps_; QString sizeString(QString().setNum(size, 'g', 3) + "em"); const QWebElementCollection spans = lastElement_.findAll("span.swift_resizable"); Q_FOREACH (QWebElement span, spans) { span.setStyleProperty("font-size", sizeString); } } //qDebug() << "-----------------"; //qDebug() << webPage_->mainFrame()->toHtml(); } void QtWebKitChatView::addLastSeenLine() { /* if the line is added we should break the snippet */ insertingLastLine_ = true; if (lineSeparator_.isNull()) { lineSeparator_ = newInsertPoint_.clone(); lineSeparator_.setInnerXml(QString("<hr/>")); newInsertPoint_.prependOutside(lineSeparator_); } else { QWebElement lineSeparatorC = lineSeparator_.clone(); lineSeparatorC.removeFromDocument(); } newInsertPoint_.prependOutside(lineSeparator_); } void QtWebKitChatView::replaceLastMessage(const QString& newMessage, const ChatWindow::TimestampBehaviour timestampBehaviour) { assert(viewReady_); rememberScrolledToBottom(); assert(!lastElement_.isNull()); QWebElement replace = lastElement_.findFirst("span.swift_message"); assert(!replace.isNull()); QString old = lastElement_.toOuterXml(); replace.setInnerXml(ChatSnippet::escape(newMessage)); if (timestampBehaviour == ChatWindow::UpdateTimestamp) { replace = lastElement_.findFirst("span.swift_time"); assert(!replace.isNull()); replace.setInnerXml(ChatSnippet::timeToEscapedString(QDateTime::currentDateTime())); } } void QtWebKitChatView::replaceLastMessage(const QString& newMessage, const QString& note) { rememberScrolledToBottom(); replaceLastMessage(newMessage, ChatWindow::KeepTimestamp); QWebElement replace = lastElement_.findFirst("span.swift_time"); assert(!replace.isNull()); replace.setInnerXml(ChatSnippet::escape(note)); } QString QtWebKitChatView::getLastSentMessage() { return lastElement_.toPlainText(); } void QtWebKitChatView::addToJSEnvironment(const QString& name, QObject* obj) { webView_->page()->currentFrame()->addToJavaScriptWindowObject(name, obj); } void QtWebKitChatView::replaceMessage(const QString& newMessage, const QString& id, const QDateTime& editTime) { rememberScrolledToBottom(); QWebElement message = document_.findFirst("#" + id); if (!message.isNull()) { QWebElement replaceContent = message.findFirst("span.swift_inner_message"); assert(!replaceContent.isNull()); QString old = replaceContent.toOuterXml(); replaceContent.setInnerXml(ChatSnippet::escape(newMessage)); QWebElement replaceTime = message.findFirst("span.swift_time"); assert(!replaceTime.isNull()); old = replaceTime.toOuterXml(); replaceTime.setInnerXml(ChatSnippet::escape(tr("%1 edited").arg(ChatSnippet::timeToEscapedString(editTime)))); } else { qWarning() << "Trying to replace element with id " << id << " but it's not there."; } } void QtWebKitChatView::showEmoticons(bool show) { showEmoticons_ = show; { const QWebElementCollection spans = document_.findAll("span.swift_emoticon_image"); Q_FOREACH (QWebElement span, spans) { span.setStyleProperty("display", show ? "inline" : "none"); } } { const QWebElementCollection spans = document_.findAll("span.swift_emoticon_text"); Q_FOREACH (QWebElement span, spans) { span.setStyleProperty("display", show ? "none" : "inline"); } } } void QtWebKitChatView::copySelectionToClipboard() { if (!webPage_->selectedText().isEmpty()) { webPage_->triggerAction(QWebPage::Copy); } } void QtWebKitChatView::setAckXML(const QString& id, const QString& xml) { QWebElement message = document_.findFirst("#" + id); /* Deliberately not asserting here, so that when we start expiring old messages it won't hit us */ if (message.isNull()) return; QWebElement ackElement = message.findFirst("span.swift_ack"); assert(!ackElement.isNull()); ackElement.setInnerXml(xml); } void QtWebKitChatView::setReceiptXML(const QString& id, const QString& xml) { QWebElement message = document_.findFirst("#" + id); if (message.isNull()) return; QWebElement receiptElement = message.findFirst("span.swift_receipt"); assert(!receiptElement.isNull()); receiptElement.setInnerXml(xml); } void QtWebKitChatView::displayReceiptInfo(const QString& id, bool showIt) { QWebElement message = document_.findFirst("#" + id); if (message.isNull()) return; QWebElement receiptElement = message.findFirst("span.swift_receipt"); assert(!receiptElement.isNull()); receiptElement.setStyleProperty("display", showIt ? "inline" : "none"); } void QtWebKitChatView::rememberScrolledToBottom() { isAtBottom_ = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) >= (webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical) - 1); } void QtWebKitChatView::scrollToBottom() { isAtBottom_ = true; webPage_->mainFrame()->setScrollBarValue(Qt::Vertical, webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical)); webView_->update(); /* Work around redraw bug in some versions of Qt. */ } void QtWebKitChatView::handleFrameSizeChanged() { if (topMessageAdded_) { // adjust new scrollbar position int newMaximum = webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); webPage_->mainFrame()->setScrollBarValue(Qt::Vertical, newMaximum - scrollBarMaximum_); topMessageAdded_ = false; } if (isAtBottom_ && !disableAutoScroll_) { scrollToBottom(); } } void QtWebKitChatView::handleLinkClicked(const QUrl& url) { QDesktopServices::openUrl(url); } void QtWebKitChatView::handleViewLoadFinished(bool ok) { Q_ASSERT(ok); viewReady_ = true; } void QtWebKitChatView::increaseFontSize(int numSteps) { //qDebug() << "Increasing"; fontSizeSteps_ += numSteps; emit fontResized(fontSizeSteps_); } void QtWebKitChatView::decreaseFontSize() { fontSizeSteps_--; if (fontSizeSteps_ < 0) { fontSizeSteps_ = 0; } emit fontResized(fontSizeSteps_); } void QtWebKitChatView::resizeFont(int fontSizeSteps) { fontSizeSteps_ = fontSizeSteps; double size = 1.0 + 0.2 * fontSizeSteps_; QString sizeString(QString().setNum(size, 'g', 3) + "em"); //qDebug() << "Setting to " << sizeString; const QWebElementCollection spans = document_.findAll("span.swift_resizable"); Q_FOREACH (QWebElement span, spans) { span.setStyleProperty("font-size", sizeString); } webView_->setFontSizeIsMinimal(size == 1.0); } void QtWebKitChatView::resetView() { lastElement_ = QWebElement(); firstElement_ = lastElement_; topMessageAdded_ = false; scrollBarMaximum_ = 0; QString pageHTML = theme_->getTemplate(); pageHTML.replace("==bodyBackground==", "background-color:#e3e3e3"); pageHTML.replace(pageHTML.indexOf("%@"), 2, theme_->getBase()); if (pageHTML.count("%@") > 3) { pageHTML.replace(pageHTML.indexOf("%@"), 2, theme_->getMainCSS()); } pageHTML.replace(pageHTML.indexOf("%@"), 2, "Variants/Blue on Green.css"); pageHTML.replace(pageHTML.indexOf("%@"), 2, ""/*headerSnippet.getContent()*/); pageHTML.replace(pageHTML.indexOf("%@"), 2, ""/*footerSnippet.getContent()*/); QEventLoop syncLoop; connect(webView_, SIGNAL(loadFinished(bool)), &syncLoop, SLOT(quit())); webPage_->mainFrame()->setHtml(pageHTML); while (!viewReady_) { QTimer t; t.setSingleShot(true); connect(&t, SIGNAL(timeout()), &syncLoop, SLOT(quit())); t.start(50); syncLoop.exec(); } document_ = webPage_->mainFrame()->documentElement(); resetTopInsertPoint(); QWebElement chatElement = document_.findFirst("#Chat"); newInsertPoint_ = chatElement.clone(); newInsertPoint_.setOuterXml("<div id='swift_insert'/>"); chatElement.appendInside(newInsertPoint_); Q_ASSERT(!newInsertPoint_.isNull()); scrollToBottom(); connect(webPage_->mainFrame(), SIGNAL(contentsSizeChanged(const QSize&)), this, SLOT(handleFrameSizeChanged()), Qt::UniqueConnection); } static QWebElement findElementWithID(QWebElement document, QString elementName, QString id) { QWebElementCollection elements = document.findAll(elementName); Q_FOREACH(QWebElement element, elements) { if (element.attribute("id") == id) { return element; } } return QWebElement(); } void QtWebKitChatView::setFileTransferProgress(QString id, const int percentageDone) { QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { SWIFT_LOG(debug) << "Tried to access FT UI via invalid id!" << std::endl; return; } QWebElement progressBar = ftElement.findFirst("div.progressbar"); progressBar.setStyleProperty("width", QString::number(percentageDone) + "%"); QWebElement progressBarValue = ftElement.findFirst("div.progressbar-value"); progressBarValue.setInnerXml(QString::number(percentageDone) + " %"); } void QtWebKitChatView::setFileTransferStatus(QString id, const ChatWindow::FileTransferState state, const QString& /* msg */) { QWebElement ftElement = findElementWithID(document_, "div", id); if (ftElement.isNull()) { SWIFT_LOG(debug) << "Tried to access FT UI via invalid id! id = " << Q2PSTRING(id) << std::endl; return; } QString newInnerHTML = ""; if (state == ChatWindow::WaitingForAccept) { newInnerHTML = tr("Waiting for other side to accept the transfer.") + "<br/>" + buildChatWindowButton(tr("Cancel"), ButtonFileTransferCancel, id); } if (state == ChatWindow::Negotiating) { // replace with text "Negotiaging" + Cancel button newInnerHTML = tr("Negotiating...") + "<br/>" + buildChatWindowButton(tr("Cancel"), ButtonFileTransferCancel, id); } else if (state == ChatWindow::Transferring) { // progress bar + Cancel Button newInnerHTML = "<div style=\"position: relative; width: 90%; height: 20px; border: 2px solid grey; -webkit-border-radius: 10px;\">" "<div class=\"progressbar\" style=\"width: 0%; height: 100%; background: #AAA; -webkit-border-radius: 6px;\">" "<div class=\"progressbar-value\" style=\"position: absolute; top: 0px; left: 0px; width: 100%; text-align: center; padding-top: 2px;\">" "0%" "</div>" "</div>" "</div>" + buildChatWindowButton(tr("Cancel"), ButtonFileTransferCancel, id); } else if (state == ChatWindow::Canceled) { newInnerHTML = tr("Transfer has been canceled!"); } else if (state == ChatWindow::Finished) { // text "Successful transfer" newInnerHTML = tr("Transfer completed successfully."); } else if (state == ChatWindow::FTFailed) { newInnerHTML = tr("Transfer failed."); } ftElement.setInnerXml(newInnerHTML); } void QtWebKitChatView::setWhiteboardSessionStatus(QString id, const ChatWindow::WhiteboardSessionState state) { QWebElement divElement = findElementWithID(document_, "div", id); QString newInnerHTML; if (state == ChatWindow::WhiteboardAccepted) { newInnerHTML = tr("Started whiteboard chat") + "<br/>" + buildChatWindowButton(tr("Show whiteboard"), ButtonWhiteboardShowWindow, id); } else if (state == ChatWindow::WhiteboardTerminated) { newInnerHTML = tr("Whiteboard chat has been canceled"); } else if (state == ChatWindow::WhiteboardRejected) { newInnerHTML = tr("Whiteboard chat request has been rejected"); } divElement.setInnerXml(newInnerHTML); } void QtWebKitChatView::setMUCInvitationJoined(QString id) { QWebElement divElement = findElementWithID(document_, "div", id); QWebElement buttonElement = findElementWithID(divElement, "input", "mucinvite"); if (!buttonElement.isNull()) { buttonElement.setAttribute("value", tr("Return to room")); } } void QtWebKitChatView::handleScrollRequested(int, int dy, const QRect&) { rememberScrolledToBottom(); int pos = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) - dy; emit scrollRequested(pos); if (pos == 0) { emit scrollReachedTop(); } else if (pos == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical)) { emit scrollReachedBottom(); } } int QtWebKitChatView::getSnippetPositionByDate(const QDate& date) { QWebElement message = webPage_->mainFrame()->documentElement().findFirst(".date" + date.toString(Qt::ISODate)); return message.geometry().top(); } void QtWebKitChatView::resetTopInsertPoint() { QWebElement continuationElement = firstElement_.findFirst("#insert"); continuationElement.removeFromDocument(); firstElement_ = QWebElement(); topInsertPoint_.removeFromDocument(); QWebElement chatElement = document_.findFirst("#Chat"); topInsertPoint_ = chatElement.clone(); topInsertPoint_.setOuterXml("<div id='swift_insert'/>"); chatElement.prependInside(topInsertPoint_); } std::string QtWebKitChatView::addMessage( const ChatWindow::ChatMessage& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) { return addMessage(chatMessageToHTML(message), senderName, senderIsSelf, label, avatarPath, "", time, highlight, ChatSnippet::getDirection(message)); } QString QtWebKitChatView::getHighlightSpanStart(const std::string& text, const std::string& background) { QString ecsapeColor = QtUtilities::htmlEscape(P2QSTRING(text)); QString escapeBackground = QtUtilities::htmlEscape(P2QSTRING(background)); if (ecsapeColor.isEmpty()) { ecsapeColor = "black"; } if (escapeBackground.isEmpty()) { escapeBackground = "yellow"; } return QString("<span style=\"color: %1; background: %2\">").arg(ecsapeColor).arg(escapeBackground); } QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) { return getHighlightSpanStart(highlight.getTextColor(), highlight.getTextBackground()); } QString QtWebKitChatView::chatMessageToHTML(const ChatWindow::ChatMessage& message) { QString result; foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, message.getParts()) { boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; boost::shared_ptr<ChatWindow::ChatURIMessagePart> uriPart; boost::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart; boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart; if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { QString text = QtUtilities::htmlEscape(P2QSTRING(textPart->text)); text.replace("\n","<br/>"); result += text; continue; } if ((uriPart = boost::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(part))) { QString uri = QtUtilities::htmlEscape(P2QSTRING(uriPart->target)); result += "<a href='" + uri + "' >" + uri + "</a>"; continue; } if ((emoticonPart = boost::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(part))) { QString textStyle = showEmoticons_ ? "style='display:none'" : ""; QString imageStyle = showEmoticons_ ? "" : "style='display:none'"; result += "<span class='swift_emoticon_image' " + imageStyle + "><img src='" + P2QSTRING(emoticonPart->imagePath) + "'/></span><span class='swift_emoticon_text' " + textStyle + ">" + QtUtilities::htmlEscape(P2QSTRING(emoticonPart->alternativeText)) + "</span>"; continue; } if ((highlightPart = boost::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(part))) { QString spanStart = getHighlightSpanStart(highlightPart->foregroundColor, highlightPart->backgroundColor); result += spanStart + QtUtilities::htmlEscape(P2QSTRING(highlightPart->text)) + "</span>"; continue; } } return result; } std::string QtWebKitChatView::addMessage( const QString& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const QString& style, const boost::posix_time::ptime& time, const HighlightAction& highlight, ChatSnippet::Direction direction) { QString scaledAvatarPath = QtScaledAvatarCache(32).getScaledAvatarPath(avatarPath.c_str()); QString htmlString; if (label) { htmlString = QString("<span style=\"border: thin dashed grey; padding-left: .5em; padding-right: .5em; color: %1; background-color: %2; font-size: 90%; margin-right: .5em; \" class='swift_label'>").arg(QtUtilities::htmlEscape(P2QSTRING(label->getForegroundColor()))).arg(QtUtilities::htmlEscape(P2QSTRING(label->getBackgroundColor()))); htmlString += QString("%1</span> ").arg(QtUtilities::htmlEscape(P2QSTRING(label->getDisplayMarking()))); } QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightText() ? "</span>" : ""; + QString highlightSpanStart = highlight.highlightAllText() ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = highlight.highlightAllText() ? "</span>" : ""; htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "</span>" ; bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf); QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(boost::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id), direction)); previousMessageWasSelf_ = senderIsSelf; previousSenderName_ = P2QSTRING(senderName); previousMessageKind_ = PreviousMessageWasMessage; return id; } std::string QtWebKitChatView::addAction(const ChatWindow::ChatMessage& message, const std::string &senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) { return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, highlight, ChatSnippet::getDirection(message)); } static QString encodeButtonArgument(const QString& str) { return QtUtilities::htmlEscape(P2QSTRING(Base64::encode(createByteArray(Q2PSTRING(str))))); } static QString decodeButtonArgument(const QString& str) { return P2QSTRING(byteArrayToString(Base64::decode(Q2PSTRING(str)))); } QString QtWebKitChatView::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3, const QString& arg4, const QString& arg5) { QRegExp regex("[A-Za-z][A-Za-z0-9\\-\\_]+"); Q_ASSERT(regex.exactMatch(id)); QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\", \"%6\", \"%7\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)).arg(encodeButtonArgument(arg4)).arg(encodeButtonArgument(arg5)); return html; } std::string QtWebKitChatView::addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) { SWIFT_LOG(debug) << "addFileTransfer" << std::endl; QString ft_id = QString("ft%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); QString actionText; QString htmlString; QString formattedFileSize = P2QSTRING(formatSize(sizeInBytes)); if (senderIsSelf) { // outgoing actionText = tr("Send file"); htmlString = actionText + ": " + P2QSTRING(filename) + " ( " + formattedFileSize + ") <br/>" + "<div id='" + ft_id + "'>" + buildChatWindowButton(tr("Cancel"), ButtonFileTransferCancel, ft_id) + buildChatWindowButton(tr("Set Description"), ButtonFileTransferSetDescription, ft_id) + buildChatWindowButton(tr("Send"), ButtonFileTransferSendRequest, ft_id) + "</div>"; } else { // incoming actionText = tr("Receiving file"); htmlString = actionText + ": " + P2QSTRING(filename) + " ( " + formattedFileSize + ") <br/>" + "<div id='" + ft_id + "'>" + buildChatWindowButton(tr("Cancel"), ButtonFileTransferCancel, ft_id) + buildChatWindowButton(tr("Accept"), ButtonFileTransferAcceptRequest, ft_id, P2QSTRING(filename)) + "</div>"; } //addMessage(message, senderName, senderIsSelf, boost::shared_ptr<SecurityLabel>(), "", boost::posix_time::second_clock::local_time()); bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasFileTransfer, senderName, senderIsSelf); QString qAvatarPath = "qrc:/icons/avatar.png"; std::string id = "ftmessage" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(boost::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id), ChatSnippet::getDirection(actionText))); previousMessageWasSelf_ = senderIsSelf; previousSenderName_ = P2QSTRING(senderName); previousMessageKind_ = PreviousMessageWasFileTransfer; return Q2PSTRING(ft_id); } void QtWebKitChatView::setFileTransferProgress(std::string id, const int percentageDone) { setFileTransferProgress(P2QSTRING(id), percentageDone); } void QtWebKitChatView::setFileTransferStatus(std::string id, const ChatWindow::FileTransferState state, const std::string& msg) { setFileTransferStatus(P2QSTRING(id), state, P2QSTRING(msg)); } std::string QtWebKitChatView::addWhiteboardRequest(const QString& contact, bool senderIsSelf) { QString wb_id = QString("wb%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); QString htmlString; QString actionText; if (senderIsSelf) { actionText = tr("Starting whiteboard chat"); htmlString = "<div id='" + wb_id + "'>" + actionText + "<br />"+ buildChatWindowButton(tr("Cancel"), ButtonWhiteboardSessionCancel, wb_id) + "</div>"; } else { actionText = tr("%1 would like to start a whiteboard chat"); htmlString = "<div id='" + wb_id + "'>" + actionText.arg(QtUtilities::htmlEscape(contact)) + ": <br/>" + buildChatWindowButton(tr("Cancel"), ButtonWhiteboardSessionCancel, wb_id) + buildChatWindowButton(tr("Accept"), ButtonWhiteboardSessionAcceptRequest, wb_id) + "</div>"; } QString qAvatarPath = "qrc:/icons/avatar.png"; std::string id = "wbmessage" + boost::lexical_cast<std::string>(idCounter_++); addMessageBottom(boost::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(contact), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, false, false, theme_, P2QSTRING(id), ChatSnippet::getDirection(actionText))); previousMessageWasSelf_ = false; previousSenderName_ = contact; return Q2PSTRING(wb_id); } void QtWebKitChatView::setWhiteboardSessionStatus(const std::string& id, const ChatWindow::WhiteboardSessionState state) { setWhiteboardSessionStatus(P2QSTRING(id), state); } void QtWebKitChatView::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString encodedArgument2, QString encodedArgument3, QString encodedArgument4, QString encodedArgument5) { QString arg1 = decodeButtonArgument(encodedArgument1); QString arg2 = decodeButtonArgument(encodedArgument2); QString arg3 = decodeButtonArgument(encodedArgument3); QString arg4 = decodeButtonArgument(encodedArgument4); QString arg5 = decodeButtonArgument(encodedArgument5); if (id.startsWith(ButtonFileTransferCancel)) { QString ft_id = arg1; window_->onFileTransferCancel(Q2PSTRING(ft_id)); } else if (id.startsWith(ButtonFileTransferSetDescription)) { QString ft_id = arg1; bool ok = false; QString text = QInputDialog::getText(this, tr("File transfer description"), tr("Description:"), QLineEdit::Normal, "", &ok); if (ok) { descriptions_[ft_id] = text; } } else if (id.startsWith(ButtonFileTransferSendRequest)) { QString ft_id = arg1; QString text = descriptions_.find(ft_id) == descriptions_.end() ? QString() : descriptions_[ft_id]; window_->onFileTransferStart(Q2PSTRING(ft_id), Q2PSTRING(text)); } else if (id.startsWith(ButtonFileTransferAcceptRequest)) { QString ft_id = arg1; QString filename = arg2; QString path = QFileDialog::getSaveFileName(this, tr("Save File"), filename); if (!path.isEmpty()) { window_->onFileTransferAccept(Q2PSTRING(ft_id), Q2PSTRING(path)); } } else if (id.startsWith(ButtonWhiteboardSessionAcceptRequest)) { QString id = arg1; setWhiteboardSessionStatus(id, ChatWindow::WhiteboardAccepted); window_->onWhiteboardSessionAccept(); } else if (id.startsWith(ButtonWhiteboardSessionCancel)) { QString id = arg1; setWhiteboardSessionStatus(id, ChatWindow::WhiteboardTerminated); window_->onWhiteboardSessionCancel(); } else if (id.startsWith(ButtonWhiteboardShowWindow)) { QString id = arg1; window_->onWhiteboardWindowShow(); } else if (id.startsWith(ButtonMUCInvite)) { QString roomJID = arg1; QString password = arg2; QString elementID = arg3; QString isImpromptu = arg4; QString isContinuation = arg5; eventStream_->send(boost::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password), boost::optional<std::string>(), false, false, isImpromptu.contains("true"), isContinuation.contains("true"))); setMUCInvitationJoined(elementID); } else { SWIFT_LOG(debug) << "Unknown HTML button! ( " << Q2PSTRING(id) << " )" << std::endl; } } void QtWebKitChatView::addErrorMessage(const ChatWindow::ChatMessage& errorMessage) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString errorMessageHTML(chatMessageToHTML(errorMessage)); addMessageBottom(boost::make_shared<SystemMessageSnippet>("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_, ChatSnippet::getDirection(errorMessage))); previousMessageWasSelf_ = false; previousMessageKind_ = PreviousMessageWasSystem; } void QtWebKitChatView::addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML = chatMessageToHTML(message); addMessageBottom(boost::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, getActualDirection(message, direction))); previousMessageKind_ = PreviousMessageWasSystem; } void QtWebKitChatView::replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) { replaceMessage(" *" + chatMessageToHTML(message) + "*", id, time, "font-style:italic ", highlight); } void QtWebKitChatView::replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) { replaceMessage(chatMessageToHTML(message), id, time, "", highlight); } void QtWebKitChatView::replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style, const HighlightAction& highlight) { if (!id.empty()) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML(message); QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; QString styleSpanEnd = style == "" ? "" : "</span>"; - QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightText() ? "</span>" : ""; + QString highlightSpanStart = highlight.highlightAllText() ? getHighlightSpanStart(highlight) : ""; + QString highlightSpanEnd = highlight.highlightAllText() ? "</span>" : ""; messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd; replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time)); } else { std::cerr << "Trying to replace a message with no id"; } } void QtWebKitChatView::addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString messageHTML = chatMessageToHTML(message); addMessageBottom(boost::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, getActualDirection(message, direction))); previousMessageKind_ = PreviousMessageWasPresence; } void QtWebKitChatView::replaceLastMessage(const ChatWindow::ChatMessage& message, const ChatWindow::TimestampBehaviour timestampBehaviour) { replaceLastMessage(chatMessageToHTML(message), timestampBehaviour); } void QtWebKitChatView::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { if (window_->isWidgetSelected()) { window_->onAllMessagesRead(); } QString message; if (isImpromptu) { message = QObject::tr("You've been invited to join a chat.") + "\n"; } else { message = QObject::tr("You've been invited to enter the %1 room.").arg(P2QSTRING(jid.toString())) + "\n"; } QString htmlString = message; if (!reason.empty()) { htmlString += QObject::tr("Reason: %1").arg(P2QSTRING(reason)) + "\n"; } if (!direct) { htmlString += QObject::tr("This person may not have really sent this invitation!") + "\n"; } htmlString = chatMessageToHTML(ChatWindow::ChatMessage(Q2PSTRING(htmlString))); QString id = QString(ButtonMUCInvite + "%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); htmlString += "<div id='" + id + "'>" + buildChatWindowButton(chatMessageToHTML(ChatWindow::ChatMessage(Q2PSTRING((tr("Accept Invite"))))), ButtonMUCInvite, QtUtilities::htmlEscape(P2QSTRING(jid.toString())), QtUtilities::htmlEscape(P2QSTRING(password)), id, QtUtilities::htmlEscape(isImpromptu ? "true" : "false"), QtUtilities::htmlEscape(isContinuation ? "true" : "false")) + "</div>"; bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMUCInvite, senderName, false); QString qAvatarPath = "qrc:/icons/avatar.png"; addMessageBottom(boost::make_shared<MessageSnippet>(htmlString, QtUtilities::htmlEscape(P2QSTRING(senderName)), B2QDATE(boost::posix_time::second_clock::local_time()), qAvatarPath, false, appendToPrevious, theme_, id, ChatSnippet::getDirection(message))); previousMessageWasSelf_ = false; previousSenderName_ = P2QSTRING(senderName); previousMessageKind_ = PreviousMessageWasMUCInvite; } void QtWebKitChatView::setAckState(std::string const& id, ChatWindow::AckState state) { QString xml; switch (state) { case ChatWindow::Pending: xml = "<img src='qrc:/icons/throbber.gif' title='" + tr("This message has not been received by your server yet.") + "'/>"; displayReceiptInfo(P2QSTRING(id), false); break; case ChatWindow::Received: xml = ""; displayReceiptInfo(P2QSTRING(id), true); break; case ChatWindow::Failed: xml = "<img src='qrc:/icons/error.png' title='" + tr("This message may not have been transmitted.") + "'/>"; break; } setAckXML(P2QSTRING(id), xml); } void QtWebKitChatView::setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) { QString xml; switch (state) { case ChatWindow::ReceiptReceived: xml = "<img src='qrc:/icons/check.png' title='" + tr("The receipt for this message has been received.") + "'/>"; break; case ChatWindow::ReceiptRequested: xml = "<img src='qrc:/icons/warn.png' title='" + tr("The receipt for this message has not yet been received. The recipient(s) might not have received this message.") + "'/>"; break; case ChatWindow::ReceiptFailed: xml = "<img src='qrc:/icons/error.png' title='" + tr("Failed to transmit message to the receipient(s).") + "'/>"; } setReceiptXML(P2QSTRING(id), xml); } bool QtWebKitChatView::appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf) { bool result = previousMessageKind_ == messageKind && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_&& previousSenderName_ == P2QSTRING(senderName))); if (insertingLastLine_) { insertingLastLine_ = false; return false; } return result; } ChatSnippet::Direction QtWebKitChatView::getActualDirection(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) { if (direction == ChatWindow::DefaultDirection) { return QCoreApplication::translate("QApplication", "QT_LAYOUT_DIRECTION") == "RTL" ? ChatSnippet::RTL : ChatSnippet::LTR; } else { return ChatSnippet::getDirection(message); } } // void QtWebKitChatView::setShowEmoticons(bool value) { // showEmoticons_ = value; // } } |
Swift