diff options
Diffstat (limited to 'Swift/QtUI')
-rw-r--r-- | Swift/QtUI/QtChatView.cpp | 482 | ||||
-rw-r--r-- | Swift/QtUI/QtChatView.h | 125 | ||||
-rw-r--r-- | Swift/QtUI/QtChatWindow.cpp | 555 | ||||
-rw-r--r-- | Swift/QtUI/QtChatWindow.h | 54 | ||||
-rw-r--r-- | Swift/QtUI/QtHistoryWindow.cpp | 32 | ||||
-rw-r--r-- | Swift/QtUI/QtHistoryWindow.h | 27 | ||||
-rw-r--r-- | Swift/QtUI/QtWebKitChatView.cpp | 948 | ||||
-rw-r--r-- | Swift/QtUI/QtWebKitChatView.h | 187 | ||||
-rw-r--r-- | Swift/QtUI/SConscript | 3 |
9 files changed, 1314 insertions, 1099 deletions
diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp index 31b9915..db4fe51 100644 --- a/Swift/QtUI/QtChatView.cpp +++ b/Swift/QtUI/QtChatView.cpp @@ -1,492 +1,20 @@ /* - * Copyright (c) 2010 Remko Tronçon + * Copyright (c) 2013 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#include "QtChatView.h" - -#include <QtDebug> -#include <QEventLoop> -#include <QFile> -#include <QDesktopServices> -#include <QVBoxLayout> -#include <QWebFrame> -#include <QKeyEvent> -#include <QStackedWidget> -#include <QTimer> -#include <QMessageBox> -#include <QApplication> - -#include <Swiften/Base/Log.h> - -#include "QtWebView.h" -#include "QtChatTheme.h" -#include "QtChatWindow.h" -#include "QtSwiftUtil.h" +#include <Swift/QtUI/QtChatView.h> namespace Swift { -QtChatView::QtChatView(QtChatTheme* theme, QWidget* parent, bool disableAutoScroll) : QWidget(parent), fontSizeSteps_(0), disableAutoScroll_(disableAutoScroll) { - theme_ = theme; - - QVBoxLayout* mainLayout = new QVBoxLayout(this); - mainLayout->setSpacing(0); - mainLayout->setContentsMargins(0,0,0,0); - webView_ = new QtWebView(this); - connect(webView_, SIGNAL(linkClicked(const QUrl&)), SLOT(handleLinkClicked(const QUrl&))); - connect(webView_, SIGNAL(loadFinished(bool)), SLOT(handleViewLoadFinished(bool))); - connect(webView_, SIGNAL(gotFocus()), SIGNAL(gotFocus())); - connect(webView_, SIGNAL(clearRequested()), SLOT(handleClearRequested())); - connect(webView_, SIGNAL(fontGrowRequested()), SLOT(increaseFontSize())); - connect(webView_, SIGNAL(fontShrinkRequested()), SLOT(decreaseFontSize())); -#if defined (Q_OS_UNIX) && !defined(Q_OS_MAC) - /* To give a border on Linux, where it looks bad without */ - QStackedWidget* stack = new QStackedWidget(this); - stack->addWidget(webView_); - stack->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); - 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(); -} - -void QtChatView::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 QtChatView::handleKeyPressEvent(QKeyEvent* event) { - webView_->keyPressEvent(event); -} - -void QtChatView::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 QtChatView::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 QtChatView::snippetToDOM(boost::shared_ptr<ChatSnippet> snippet) { - QWebElement newElement = newInsertPoint_.clone(); - newElement.setInnerXml(snippet->getContent()); - Q_ASSERT(!newElement.isNull()); - return newElement; -} - -void QtChatView::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 QtChatView::addLastSeenLine() { - if (lineSeparator_.isNull()) { - lineSeparator_ = newInsertPoint_.clone(); - lineSeparator_.setInnerXml(QString("<hr/>")); - newInsertPoint_.prependOutside(lineSeparator_); - } - else { - QWebElement lineSeparatorC = lineSeparator_.clone(); - lineSeparatorC.removeFromDocument(); - } - newInsertPoint_.prependOutside(lineSeparator_); -} - -void QtChatView::replaceLastMessage(const QString& newMessage) { - 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)); -} - -void QtChatView::replaceLastMessage(const QString& newMessage, const QString& note) { - rememberScrolledToBottom(); - replaceLastMessage(newMessage); - QWebElement replace = lastElement_.findFirst("span.swift_time"); - assert(!replace.isNull()); - replace.setInnerXml(ChatSnippet::escape(note)); -} - -QString QtChatView::getLastSentMessage() { - return lastElement_.toPlainText(); -} - -void QtChatView::addToJSEnvironment(const QString& name, QObject* obj) { - webView_->page()->currentFrame()->addToJavaScriptWindowObject(name, obj); -} - -void QtChatView::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 QtChatView::showEmoticons(bool 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 QtChatView::copySelectionToClipboard() { - if (!webPage_->selectedText().isEmpty()) { - webPage_->triggerAction(QWebPage::Copy); - } -} - -void QtChatView::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 QtChatView::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 QtChatView::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 QtChatView::rememberScrolledToBottom() { - isAtBottom_ = webPage_->mainFrame()->scrollBarValue(Qt::Vertical) >= (webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical) - 1); -} - -void QtChatView::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 QtChatView::handleFrameSizeChanged() { - if (topMessageAdded_) { - // adjust new scrollbar position - int newMaximum = webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); - webPage_->mainFrame()->setScrollBarValue(Qt::Vertical, newMaximum - scrollBarMaximum_); - topMessageAdded_ = false; - } +QtChatView::QtChatView(QWidget* parent) : QWidget(parent) { - if (isAtBottom_ && !disableAutoScroll_) { - scrollToBottom(); - } } -void QtChatView::handleLinkClicked(const QUrl& url) { - QDesktopServices::openUrl(url); -} - -void QtChatView::handleViewLoadFinished(bool ok) { - Q_ASSERT(ok); - viewReady_ = true; -} - -void QtChatView::increaseFontSize(int numSteps) { - //qDebug() << "Increasing"; - fontSizeSteps_ += numSteps; - emit fontResized(fontSizeSteps_); -} - -void QtChatView::decreaseFontSize() { - fontSizeSteps_--; - if (fontSizeSteps_ < 0) { - fontSizeSteps_ = 0; - } - emit fontResized(fontSizeSteps_); -} - -void QtChatView::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 QtChatView::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 QtChatView::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 QtChatView::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/>" + - QtChatWindow::buildChatWindowButton(tr("Cancel"), QtChatWindow::ButtonFileTransferCancel, id); - } - if (state == ChatWindow::Negotiating) { - // replace with text "Negotiaging" + Cancel button - newInnerHTML = tr("Negotiating...") + "<br/>" + - QtChatWindow::buildChatWindowButton(tr("Cancel"), QtChatWindow::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>" + - QtChatWindow::buildChatWindowButton(tr("Cancel"), QtChatWindow::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 QtChatView::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/>" + - QtChatWindow::buildChatWindowButton(tr("Show whiteboard"), QtChatWindow::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 QtChatView::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 QtChatView::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 QtChatView::getSnippetPositionByDate(const QDate& date) { - QWebElement message = webPage_->mainFrame()->documentElement().findFirst(".date" + date.toString(Qt::ISODate)); - - return message.geometry().top(); -} - -void QtChatView::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_); +QtChatView::~QtChatView() { + } } diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h index 9080808..c8519b7 100644 --- a/Swift/QtUI/QtChatView.h +++ b/Swift/QtUI/QtChatView.h @@ -1,101 +1,62 @@ /* - * Copyright (c) 2010 Remko Tronçon + * Copyright (c) 2013 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#ifndef SWIFT_QtChatView_H -#define SWIFT_QtChatView_H - -#include <QString> -#include <QWidget> -#include <QList> -#include <QWebElement> +#pragma once +#include <string> #include <boost/shared_ptr.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> -#include "ChatSnippet.h" +#include <QWidget> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> -class QWebPage; -class QUrl; -class QDate; - namespace Swift { - class QtWebView; - class QtChatTheme; + class HighlightAction; + class SecurityLabel; + class QtChatView : public QWidget { - Q_OBJECT + Q_OBJECT public: - QtChatView(QtChatTheme* theme, QWidget* parent, bool disableAutoScroll = false); - void addMessageTop(boost::shared_ptr<ChatSnippet> snippet); - void addMessageBottom(boost::shared_ptr<ChatSnippet> snippet); - void addLastSeenLine(); - void replaceLastMessage(const QString& newMessage); - void replaceLastMessage(const QString& newMessage, const QString& note); - void replaceMessage(const QString& newMessage, const QString& id, const QDateTime& time); - void rememberScrolledToBottom(); - void setAckXML(const QString& id, const QString& xml); - void setReceiptXML(const QString& id, const QString& xml); - void displayReceiptInfo(const QString& id, bool showIt); - - QString getLastSentMessage(); - void addToJSEnvironment(const QString&, QObject*); - void setFileTransferProgress(QString id, const int percentageDone); - void setFileTransferStatus(QString id, const ChatWindow::FileTransferState state, const QString& msg); - void setWhiteboardSessionStatus(QString id, const ChatWindow::WhiteboardSessionState state); - void setMUCInvitationJoined(QString id); - void showEmoticons(bool show); - int getSnippetPositionByDate(const QDate& date); - - signals: - void gotFocus(); - void fontResized(int); - void logCleared(); - void scrollRequested(int pos); - void scrollReachedTop(); - void scrollReachedBottom(); + QtChatView(QWidget* parent); + virtual ~QtChatView(); + + /** Add message to window. + * @return id of added message (for acks). + */ + virtual std::string 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) = 0; + /** Adds action to window. + * @return id of added message (for acks); + */ + virtual std::string 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) = 0; + + virtual void addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) = 0; + virtual void addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) = 0; + + virtual void addErrorMessage(const ChatWindow::ChatMessage& message) = 0; + virtual void replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0; + virtual void replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) = 0; + virtual void replaceLastMessage(const ChatWindow::ChatMessage& message) = 0; + virtual void setAckState(const std::string& id, ChatWindow::AckState state) = 0; + + virtual std::string addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) = 0; + virtual void setFileTransferProgress(std::string, const int percentageDone) = 0; + virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState state, const std::string& msg = "") = 0; + virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) = 0; + virtual std::string addWhiteboardRequest(const QString& contact, bool senderIsSelf) = 0; + virtual void setWhiteboardSessionStatus(std::string id, const ChatWindow::WhiteboardSessionState state) = 0; + virtual void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) = 0; + + virtual void showEmoticons(bool show) = 0; + virtual void addLastSeenLine() = 0; public slots: - void copySelectionToClipboard(); - void scrollToBottom(); - void handleLinkClicked(const QUrl&); - void handleKeyPressEvent(QKeyEvent* event); - void resetView(); - void resetTopInsertPoint(); - void increaseFontSize(int numSteps = 1); - void decreaseFontSize(); - void resizeFont(int fontSizeSteps); - - private slots: - void handleViewLoadFinished(bool); - void handleFrameSizeChanged(); - void handleClearRequested(); - void handleScrollRequested(int dx, int dy, const QRect& rectToScroll); + virtual void resizeFont(int fontSizeSteps) = 0; + virtual void scrollToBottom() = 0; + virtual void handleKeyPressEvent(QKeyEvent* event) = 0; - private: - void headerEncode(); - void messageEncode(); - void addToDOM(boost::shared_ptr<ChatSnippet> snippet); - QWebElement snippetToDOM(boost::shared_ptr<ChatSnippet> snippet); - - bool viewReady_; - bool isAtBottom_; - bool topMessageAdded_; - int scrollBarMaximum_; - QtWebView* webView_; - QWebPage* webPage_; - int fontSizeSteps_; - QtChatTheme* theme_; - QWebElement newInsertPoint_; - QWebElement topInsertPoint_; - QWebElement lineSeparator_; - QWebElement lastElement_; - QWebElement firstElement_; - QWebElement document_; - bool disableAutoScroll_; }; } - -#endif diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index 2dfef5a..2b68475 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -10,24 +10,19 @@ #include "Swift/Controllers/Roster/ContactRosterItem.h" #include "Roster/QtOccupantListWidget.h" #include "SwifTools/Linkify.h" -#include "QtChatView.h" -#include "MessageSnippet.h" -#include "SystemMessageSnippet.h" +#include "QtWebKitChatView.h" #include "QtTextEdit.h" #include "QtSettingsProvider.h" #include "QtScaledAvatarCache.h" #include <Swift/QtUI/QtUISettingConstants.h> -#include <Swiften/StringCodecs/Base64.h> #include "SwifTools/TabComplete.h" #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIEvents/SendFileUIEvent.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> -#include "QtChatWindowJSBridge.h" #include "QtUtilities.h" #include <boost/cstdint.hpp> -#include <boost/format.hpp> #include <boost/smart_ptr/make_shared.hpp> #include <boost/lexical_cast.hpp> @@ -57,20 +52,9 @@ namespace Swift { -const QString QtChatWindow::ButtonWhiteboardSessionCancel = QString("whiteboard-cancel"); -const QString QtChatWindow::ButtonWhiteboardSessionAcceptRequest = QString("whiteboard-acceptrequest"); -const QString QtChatWindow::ButtonWhiteboardShowWindow = QString("whiteboard-showwindow"); -const QString QtChatWindow::ButtonFileTransferCancel = QString("filetransfer-cancel"); -const QString QtChatWindow::ButtonFileTransferSetDescription = QString("filetransfer-setdescription"); -const QString QtChatWindow::ButtonFileTransferSendRequest = QString("filetransfer-sendrequest"); -const QString QtChatWindow::ButtonFileTransferAcceptRequest = QString("filetransfer-acceptrequest"); -const QString QtChatWindow::ButtonMUCInvite = QString("mucinvite"); - - -QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageKind_(PreviosuMessageWasNone), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { +QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { settings_ = settings; unreadCount_ = 0; - idCounter_ = 0; inputEnabled_ = true; completer_ = NULL; affiliationEditor_ = NULL; @@ -78,7 +62,6 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt isCorrection_ = false; labelModel_ = NULL; correctionEnabled_ = Maybe; - showEmoticons_ = true; updateTitleWithUnreadCount(); #ifdef SWIFT_EXPERIMENTAL_FT @@ -122,7 +105,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt logRosterSplitter_ = new QSplitter(this); logRosterSplitter_->setAutoFillBackground(true); layout->addWidget(logRosterSplitter_); - messageLog_ = new QtChatView(theme, this); + messageLog_ = new QtWebKitChatView(this, eventStream_, theme, this); // I accept that passing the ChatWindow in so that the view can call the signals is somewhat inelegant, but it saves a lot of boilerplate. This patch is unpleasant enough already. So let's fix this soon (it at least needs fixing by the time history is sorted), but not now. logRosterSplitter_->addWidget(messageLog_); treeWidget_ = new QtOccupantListWidget(eventStream_, settings_, this); @@ -186,16 +169,12 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt treeWidget_->onSomethingSelectedChanged.connect(boost::bind(&QtChatWindow::handleOccupantSelectionChanged, this, _1)); treeWidget_->onOccupantActionSelected.connect(boost::bind(boost::ref(onOccupantActionSelected), _1, _2)); - jsBridge = new QtChatWindowJSBridge(); - messageLog_->addToJSEnvironment("chatwindow", jsBridge); - connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString,QString,QString))); - settings_->onSettingChanged.connect(boost::bind(&QtChatWindow::handleSettingChanged, this, _1)); - showEmoticons_ = settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS); + messageLog_->showEmoticons(settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS)); + } QtChatWindow::~QtChatWindow() { - delete jsBridge; if (mucConfigurationWindow_) { delete mucConfigurationWindow_.data(); } @@ -203,8 +182,8 @@ QtChatWindow::~QtChatWindow() { void QtChatWindow::handleSettingChanged(const std::string& setting) { if (setting == QtUISettingConstants::SHOW_EMOTICONS.getKey()) { - showEmoticons_ = settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS); - messageLog_->showEmoticons(showEmoticons_); + bool showEmoticons = settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS); + messageLog_->showEmoticons(showEmoticons); } } @@ -216,19 +195,6 @@ void QtChatWindow::handleOccupantSelectionChanged(RosterItem* item) { onOccupantSelectionChanged(dynamic_cast<ContactRosterItem*>(item)); } -bool QtChatWindow::appendToPreviousCheck(QtChatWindow::PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf) const { - return previousMessageKind_ == messageKind && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_&& previousSenderName_ == P2QSTRING(senderName))); -} - -ChatSnippet::Direction QtChatWindow::getActualDirection(const ChatMessage& message, Direction direction) { - if (direction == DefaultDirection) { - return QCoreApplication::translate("QApplication", "QT_LAYOUT_DIRECTION") == "RTL" ? ChatSnippet::RTL : ChatSnippet::LTR; - } - else { - return ChatSnippet::getDirection(message); - } -} - void QtChatWindow::handleFontResized(int fontSizeSteps) { messageLog_->resizeFont(fontSizeSteps); } @@ -495,412 +461,16 @@ void QtChatWindow::updateTitleWithUnreadCount() { emit titleUpdated(); } -std::string QtChatWindow::addMessage( - const 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 QtChatWindow::chatMessageToHTML(const ChatMessage& message) { - QString result; - foreach (boost::shared_ptr<ChatMessagePart> part, message.getParts()) { - boost::shared_ptr<ChatTextMessagePart> textPart; - boost::shared_ptr<ChatURIMessagePart> uriPart; - boost::shared_ptr<ChatEmoticonMessagePart> emoticonPart; - boost::shared_ptr<ChatHighlightingMessagePart> highlightPart; - - if ((textPart = boost::dynamic_pointer_cast<ChatTextMessagePart>(part))) { - QString text = QtUtilities::htmlEscape(P2QSTRING(textPart->text)); - text.replace("\n","<br/>"); - result += text; - continue; - } - if ((uriPart = boost::dynamic_pointer_cast<ChatURIMessagePart>(part))) { - QString uri = QtUtilities::htmlEscape(P2QSTRING(uriPart->target)); - result += "<a href='" + uri + "' >" + uri + "</a>"; - continue; - } - if ((emoticonPart = boost::dynamic_pointer_cast<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<ChatHighlightingMessagePart>(part))) { - //FIXME: Maybe do something here. Anything, really. - continue; - } - - } - return result; -} - -/*QString QtChatWindow::linkimoticonify(const std::string& message) const { - return linkimoticonify(P2QSTRING(message)); -} - -QString QtChatWindow::linkimoticonify(const QString& message) const { - QString messageHTML(message); - messageHTML = QtUtilities::htmlEscape(messageHTML); - QMapIterator<QString, QString> it(emoticons_); - - if (messageHTML.length() < 500) { - while (it.hasNext()) { - it.next(); - messageHTML.replace(it.key(), ); - } - messageHTML = P2QSTRING(Linkify::linkify(Q2PSTRING(messageHTML))); - } - messageHTML.replace("\n","<br/>"); - return messageHTML; -}*/ - -QString QtChatWindow::getHighlightSpanStart(const HighlightAction& highlight) { - QString color = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextColor())); - QString background = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextBackground())); - if (color.isEmpty()) { - color = "black"; - } - if (background.isEmpty()) { - background = "yellow"; - } - - return QString("<span style=\"color: %1; background: %2\">").arg(color).arg(background); -} - -std::string QtChatWindow::addMessage( - const QString& message, - const std::string& senderName, - bool senderIsSelf, - boost::shared_ptr<SecurityLabel> label, - const std::string& avatarPath, - const QString& style, - const boost::posix_time::ptime& time, - const HighlightAction& highlight, - ChatSnippet::Direction direction) { - - if (isWidgetSelected()) { - onAllMessagesRead(); - } - 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>" : ""; - htmlString += "<span class='swift_inner_message'>" + styleSpanStart + highlightSpanStart + message + highlightSpanEnd + styleSpanEnd + "</span>" ; - - bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMessage, senderName, senderIsSelf); - if (lastLineTracker_.getShouldMoveLastLine()) { - /* should this be queued? */ - messageLog_->addLastSeenLine(); - /* if the line is added we should break the snippet */ - appendToPrevious = false; - } - QString qAvatarPath = scaledAvatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(scaledAvatarPath).toEncoded(); - std::string id = "id" + boost::lexical_cast<std::string>(idCounter_++); - messageLog_->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; -} void QtChatWindow::flash() { emit requestFlash(); } -void QtChatWindow::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.") + "'/>"; - messageLog_->displayReceiptInfo(P2QSTRING(id), false); - break; - case ChatWindow::Received: - xml = ""; - messageLog_->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; - } - messageLog_->setAckXML(P2QSTRING(id), xml); -} - -void QtChatWindow::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; - } - messageLog_->setReceiptXML(P2QSTRING(id), xml); -} - int QtChatWindow::getCount() { return unreadCount_; } -std::string QtChatWindow::addAction(const 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)); -} - -// FIXME: Move this to a different file -std::string formatSize(const boost::uintmax_t bytes) { - static const char *siPrefix[] = {"k", "M", "G", "T", "P", "E", "Z", "Y", NULL}; - int power = 0; - double engBytes = bytes; - while (engBytes >= 1000) { - ++power; - engBytes = engBytes / 1000.0; - } - return str( boost::format("%.1lf %sB") % engBytes % (power > 0 ? siPrefix[power-1] : "") ); -} - -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 QtChatWindow::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 QtChatWindow::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); - if (lastLineTracker_.getShouldMoveLastLine()) { - /* should this be queued? */ - messageLog_->addLastSeenLine(); - /* if the line is added we should break the snippet */ - appendToPrevious = false; - } - QString qAvatarPath = "qrc:/icons/avatar.png"; - std::string id = "ftmessage" + boost::lexical_cast<std::string>(idCounter_++); - messageLog_->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 QtChatWindow::setFileTransferProgress(std::string id, const int percentageDone) { - messageLog_->setFileTransferProgress(P2QSTRING(id), percentageDone); -} - -void QtChatWindow::setFileTransferStatus(std::string id, const FileTransferState state, const std::string& msg) { - messageLog_->setFileTransferStatus(P2QSTRING(id), state, P2QSTRING(msg)); -} - -std::string QtChatWindow::addWhiteboardRequest(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>"; - } - - if (lastLineTracker_.getShouldMoveLastLine()) { - /* should this be queued? */ - messageLog_->addLastSeenLine(); - /* if the line is added we should break the snippet */ -// appendToPrevious = false; - } - QString qAvatarPath = "qrc:/icons/avatar.png"; - std::string id = "wbmessage" + boost::lexical_cast<std::string>(idCounter_++); - messageLog_->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 QtChatWindow::setWhiteboardSessionStatus(std::string id, const ChatWindow::WhiteboardSessionState state) { - messageLog_->setWhiteboardSessionStatus(P2QSTRING(id), state); -} - -void QtChatWindow::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; - 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]; - 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()) { - onFileTransferAccept(Q2PSTRING(ft_id), Q2PSTRING(path)); - } - } - else if (id.startsWith(ButtonWhiteboardSessionAcceptRequest)) { - QString id = arg1; - messageLog_->setWhiteboardSessionStatus(id, ChatWindow::WhiteboardAccepted); - onWhiteboardSessionAccept(); - } - else if (id.startsWith(ButtonWhiteboardSessionCancel)) { - QString id = arg1; - messageLog_->setWhiteboardSessionStatus(id, ChatWindow::WhiteboardTerminated); - onWhiteboardSessionCancel(); - } - else if (id.startsWith(ButtonWhiteboardShowWindow)) { - QString id = arg1; - 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"))); - messageLog_->setMUCInvitationJoined(elementID); - } - else { - SWIFT_LOG(debug) << "Unknown HTML button! ( " << Q2PSTRING(id) << " )" << std::endl; - } -} - -void QtChatWindow::addErrorMessage(const ChatMessage& errorMessage) { - if (isWidgetSelected()) { - onAllMessagesRead(); - } - - QString errorMessageHTML(chatMessageToHTML(errorMessage)); - - messageLog_->addMessageBottom(boost::make_shared<SystemMessageSnippet>("<span class=\"error\">" + errorMessageHTML + "</span>", QDateTime::currentDateTime(), false, theme_, ChatSnippet::getDirection(errorMessage))); - - previousMessageWasSelf_ = false; - previousMessageKind_ = PreviousMessageWasSystem; -} - -void QtChatWindow::addSystemMessage(const ChatMessage& message, Direction direction) { - if (isWidgetSelected()) { - onAllMessagesRead(); - } - - QString messageHTML = chatMessageToHTML(message); - messageLog_->addMessageBottom(boost::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, getActualDirection(message, direction))); - - previousMessageKind_ = PreviousMessageWasSystem; -} - -void QtChatWindow::replaceWithAction(const 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 QtChatWindow::replaceMessage(const ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) { - replaceMessage(chatMessageToHTML(message), id, time, "", highlight); -} - -void QtChatWindow::replaceMessage(const QString& message, const std::string& id, const boost::posix_time::ptime& time, const QString& style, const HighlightAction& highlight) { - if (!id.empty()) { - if (isWidgetSelected()) { - onAllMessagesRead(); - } - - QString messageHTML(message); - - QString styleSpanStart = style == "" ? "" : "<span style=\"" + style + "\">"; - QString styleSpanEnd = style == "" ? "" : "</span>"; - QString highlightSpanStart = highlight.highlightText() ? getHighlightSpanStart(highlight) : ""; - QString highlightSpanEnd = highlight.highlightText() ? "</span>" : ""; - messageHTML = styleSpanStart + highlightSpanStart + messageHTML + highlightSpanEnd + styleSpanEnd; - - messageLog_->replaceMessage(messageHTML, P2QSTRING(id), B2QDATE(time)); - } - else { - std::cerr << "Trying to replace a message with no id"; - } -} - -void QtChatWindow::addPresenceMessage(const ChatMessage& message, Direction direction) { - if (isWidgetSelected()) { - onAllMessagesRead(); - } - - QString messageHTML = chatMessageToHTML(message); - messageLog_->addMessageBottom(boost::make_shared<SystemMessageSnippet>(messageHTML, QDateTime::currentDateTime(), false, theme_, getActualDirection(message, direction))); - - previousMessageKind_ = PreviousMessageWasPresence; -} - void QtChatWindow::returnPressed() { if (!inputEnabled_) { @@ -988,9 +558,6 @@ void QtChatWindow::dropEvent(QDropEvent *event) { } } -void QtChatWindow::replaceLastMessage(const ChatMessage& message) { - messageLog_->replaceLastMessage(chatMessageToHTML(message)); -} void QtChatWindow::setAvailableOccupantActions(const std::vector<OccupantAction>& actions) { treeWidget_->setAvailableOccupantActions(actions); @@ -1122,45 +689,91 @@ void QtChatWindow::showRoomConfigurationForm(Form::ref form) { mucConfigurationWindow_->onFormCancelled.connect(boost::bind(boost::ref(onConfigurationFormCancelled))); } -void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { +void QtChatWindow::handleAppendedToLog() { + if (lastLineTracker_.getShouldMoveLastLine()) { + /* should this be queued? */ + messageLog_->addLastSeenLine(); + } if (isWidgetSelected()) { 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(ChatMessage(Q2PSTRING(htmlString))); +void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { + handleAppendedToLog(); + messageLog_->addMUCInvitation(senderName, jid, reason, password, direct, isImpromptu, isContinuation); +} +std::string QtChatWindow::addMessage(const 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) { + handleAppendedToLog(); + return messageLog_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time, highlight); +} - QString id = QString(ButtonMUCInvite + "%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); - htmlString += "<div id='" + id + "'>" + - buildChatWindowButton(chatMessageToHTML(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>"; +std::string QtChatWindow::addAction(const 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) { + handleAppendedToLog(); + return messageLog_->addAction(message, senderName, senderIsSelf, label, avatarPath, time, highlight); +} - bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMUCInvite, senderName, false); - if (lastLineTracker_.getShouldMoveLastLine()) { - /* should this be queued? */ - messageLog_->addLastSeenLine(); - /* if the line is added we should break the snippet */ - appendToPrevious = false; - } - QString qAvatarPath = "qrc:/icons/avatar.png"; - messageLog_->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 QtChatWindow::addSystemMessage(const ChatMessage& message, Direction direction) { + handleAppendedToLog(); + messageLog_->addSystemMessage(message, direction); +} + +void QtChatWindow::addPresenceMessage(const ChatMessage& message, Direction direction) { + handleAppendedToLog(); + messageLog_->addPresenceMessage(message, direction); +} + +void QtChatWindow::addErrorMessage(const ChatMessage& message) { + handleAppendedToLog(); + messageLog_->addErrorMessage(message); +} + + +void QtChatWindow::replaceMessage(const ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) { + handleAppendedToLog(); + messageLog_->replaceMessage(message, id, time, highlight); +} + +void QtChatWindow::replaceWithAction(const ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) { + handleAppendedToLog(); + messageLog_->replaceWithAction(message, id, time, highlight); +} + +std::string QtChatWindow::addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) { + handleAppendedToLog(); + return messageLog_->addFileTransfer(senderName, senderIsSelf, filename, sizeInBytes); +} + +void QtChatWindow::setFileTransferProgress(std::string id, const int percentageDone) { + messageLog_->setFileTransferProgress(id, percentageDone); +} + +void QtChatWindow::setFileTransferStatus(std::string id, const FileTransferState state, const std::string& msg) { + messageLog_->setFileTransferStatus(id, state, msg); +} + + +std::string QtChatWindow::addWhiteboardRequest(bool senderIsSelf) { + handleAppendedToLog(); + return messageLog_->addWhiteboardRequest(contact_, senderIsSelf); +} + +void QtChatWindow::setWhiteboardSessionStatus(std::string id, const ChatWindow::WhiteboardSessionState state) { + messageLog_->setWhiteboardSessionStatus(id, state); +} + +void QtChatWindow::replaceLastMessage(const ChatMessage& message) { + messageLog_->replaceLastMessage(message); +} + +void QtChatWindow::setAckState(const std::string& id, AckState state) { + messageLog_->setAckState(id, state); +} + +void QtChatWindow::setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) { + messageLog_->setMessageReceiptState(id, state); } } diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index ba16cfe..732c234 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -77,16 +77,6 @@ namespace Swift { Q_OBJECT public: - static const QString ButtonWhiteboardSessionCancel; - static const QString ButtonWhiteboardSessionAcceptRequest; - static const QString ButtonWhiteboardShowWindow; - static const QString ButtonFileTransferCancel; - static const QString ButtonFileTransferSetDescription; - static const QString ButtonFileTransferSendRequest; - static const QString ButtonFileTransferAcceptRequest; - static const QString ButtonMUCInvite; - - public: QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings); ~QtChatWindow(); std::string addMessage(const 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); @@ -139,8 +129,6 @@ namespace Swift { void setBlockingState(BlockingState state); virtual void setCanInitiateImpromptuChats(bool supportsImpromptu); - static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString()); - public slots: void handleChangeSplitterState(QByteArray state); void handleFontResized(int fontSizeSteps); @@ -174,48 +162,19 @@ namespace Swift { void handleSplitterMoved(int pos, int index); void handleAlertButtonClicked(); void handleActionButtonClicked(); - - void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); void handleAffiliationEditorAccepted(); void handleCurrentLabelChanged(int); private: - enum PreviousMessageKind { - PreviosuMessageWasNone, - PreviousMessageWasMessage, - PreviousMessageWasSystem, - PreviousMessageWasPresence, - PreviousMessageWasFileTransfer, - PreviousMessageWasMUCInvite - }; - - private: void updateTitleWithUnreadCount(); void tabComplete(); void beginCorrection(); void cancelCorrection(); void handleSettingChanged(const std::string& setting); - std::string addMessage( - const QString& message, - const std::string& senderName, - bool senderIsSelf, - boost::shared_ptr<SecurityLabel> label, - const std::string& avatarPath, - const QString& style, - const boost::posix_time::ptime& time, - const HighlightAction& highlight, - ChatSnippet::Direction direction); - void replaceMessage( - const QString& message, - const std::string& id, - const boost::posix_time::ptime& time, - const QString& style, - const HighlightAction& highlight); + void handleOccupantSelectionChanged(RosterItem* item); - bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf) const; - static ChatSnippet::Direction getActualDirection(const ChatMessage& message, Direction direction); - QString chatMessageToHTML(const ChatMessage& message); - QString getHighlightSpanStart(const HighlightAction& highlight); + void handleAppendedToLog(); + int unreadCount_; bool contactIsTyping_; @@ -237,9 +196,6 @@ namespace Swift { TabComplete* completer_; QLineEdit* subject_; bool isCorrection_; - bool previousMessageWasSelf_; - PreviousMessageKind previousMessageKind_; - QString previousSenderName_; bool inputClearing_; bool tabCompletion_; UIEventStream* eventStream_; @@ -247,14 +203,10 @@ namespace Swift { QSplitter *logRosterSplitter_; Tristate correctionEnabled_; QString alertStyleSheet_; - std::map<QString, QString> descriptions; - QtChatWindowJSBridge* jsBridge; QPointer<QtMUCConfigurationWindow> mucConfigurationWindow_; QPointer<QtAffiliationEditor> affiliationEditor_; - int idCounter_; SettingsProvider* settings_; std::vector<ChatWindow::RoomAction> availableRoomActions_; - bool showEmoticons_; QPalette defaultLabelsPalette_; LabelModel* labelModel_; BlockingState blockingState_; diff --git a/Swift/QtUI/QtHistoryWindow.cpp b/Swift/QtUI/QtHistoryWindow.cpp index 6f22b76..9f88258 100644 --- a/Swift/QtUI/QtHistoryWindow.cpp +++ b/Swift/QtUI/QtHistoryWindow.cpp @@ -4,14 +4,18 @@ * See Documentation/Licenses/BSD-simplified.txt for more information. */ +/* + * Copyright (c) 2013 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + #include <QtHistoryWindow.h> -#include <QtTabbable.h> -#include <QtSwiftUtil.h> -#include <MessageSnippet.h> -#include <Swiften/History/HistoryMessage.h> #include <string> +#include <boost/date_time/gregorian/gregorian.hpp> +#include <boost/numeric/conversion/cast.hpp> #include <boost/shared_ptr.hpp> #include <boost/smart_ptr/make_shared.hpp> @@ -20,14 +24,20 @@ #include <QMenu> #include <QTextDocument> #include <QDateTime> -#include <Swift/QtUI/QtScaledAvatarCache.h> -#include <Swift/QtUI/ChatSnippet.h> #include <QLineEdit> -#include "QtUtilities.h" -#include <boost/smart_ptr/make_shared.hpp> -#include <boost/numeric/conversion/cast.hpp> -#include <boost/date_time/gregorian/gregorian.hpp> +#include <Swiften/History/HistoryMessage.h> + +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> + +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/MessageSnippet.h> +#include <Swift/QtUI/QtScaledAvatarCache.h> +#include <Swift/QtUI/ChatSnippet.h> +#include <Swift/QtUI/QtUtilities.h> +#include <Swift/QtUI/Roster/QtTreeWidget.h> +#include <Swift/QtUI/QtWebKitChatView.h> namespace Swift { @@ -40,7 +50,7 @@ QtHistoryWindow::QtHistoryWindow(SettingsProvider* settings, UIEventStream* even idCounter_ = 0; delete ui_.conversation_; - conversation_ = new QtChatView(theme_, this, true); + conversation_ = new QtWebKitChatView(NULL, NULL, theme_, this, true); // Horrible unsafe. Do not do this. FIXME QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); sizePolicy.setHorizontalStretch(80); sizePolicy.setVerticalStretch(0); diff --git a/Swift/QtUI/QtHistoryWindow.h b/Swift/QtUI/QtHistoryWindow.h index 49de098..fcbfd7e 100644 --- a/Swift/QtUI/QtHistoryWindow.h +++ b/Swift/QtUI/QtHistoryWindow.h @@ -4,17 +4,32 @@ * See Documentation/Licenses/BSD-simplified.txt for more information. */ +/* + * Copyright (c) 2013 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + #pragma once -#include <Swift/Controllers/UIInterfaces/HistoryWindow.h> -#include <Swift/QtUI/ui_QtHistoryWindow.h> -#include <QtChatView.h> -#include <QtTabbable.h> -#include <Swift/QtUI/Roster/QtTreeWidget.h> #include <set> + #include <QDate> +#include <Swift/Controllers/UIInterfaces/HistoryWindow.h> + +#include <Swift/QtUI/QtTabbable.h> + +#include <Swift/QtUI/ui_QtHistoryWindow.h> + namespace Swift { + class QtTabbable; + class QtTreeWidget; + class QtWebKitChatView; + class QtChatTheme; + class SettingsProvider; + class UIEventStream; + class QtHistoryWindow : public QtTabbable, public HistoryWindow { Q_OBJECT @@ -54,7 +69,7 @@ namespace Swift { Ui::QtHistoryWindow ui_; QtChatTheme* theme_; - QtChatView* conversation_; + QtWebKitChatView* conversation_; QtTreeWidget* conversationRoster_; std::set<QDate> dates_; int idCounter_; diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp new file mode 100644 index 0000000..bc57de4 --- /dev/null +++ b/Swift/QtUI/QtWebKitChatView.cpp @@ -0,0 +1,948 @@ +/* + * Copyright (c) 2010-2013 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "QtWebKitChatView.h" + +#include <boost/format.hpp> + +#include <QtDebug> +#include <QEventLoop> +#include <QFile> +#include <QDesktopServices> +#include <QVBoxLayout> +#include <QWebFrame> +#include <QKeyEvent> +#include <QStackedWidget> +#include <QTimer> +#include <QMessageBox> +#include <QApplication> +#include <QInputDialog> +#include <QFileDialog> + +#include <Swiften/Base/Log.h> +#include <Swiften/StringCodecs/Base64.h> + +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> + +#include <Swift/QtUI/QtWebView.h> +#include <Swift/QtUI/QtChatWindow.h> +#include <Swift/QtUI/QtChatWindowJSBridge.h> +#include <Swift/QtUI/QtScaledAvatarCache.h> +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/QtUtilities.h> +#include <Swift/QtUI/MessageSnippet.h> +#include <Swift/QtUI/SystemMessageSnippet.h> + +namespace Swift { + +const QString QtWebKitChatView::ButtonWhiteboardSessionCancel = QString("whiteboard-cancel"); +const QString QtWebKitChatView::ButtonWhiteboardSessionAcceptRequest = QString("whiteboard-acceptrequest"); +const QString QtWebKitChatView::ButtonWhiteboardShowWindow = QString("whiteboard-showwindow"); +const QString QtWebKitChatView::ButtonFileTransferCancel = QString("filetransfer-cancel"); +const QString QtWebKitChatView::ButtonFileTransferSetDescription = QString("filetransfer-setdescription"); +const QString QtWebKitChatView::ButtonFileTransferSendRequest = QString("filetransfer-sendrequest"); +const QString QtWebKitChatView::ButtonFileTransferAcceptRequest = QString("filetransfer-acceptrequest"); +const QString QtWebKitChatView::ButtonMUCInvite = QString("mucinvite"); + +QtWebKitChatView::QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, bool disableAutoScroll) : QtChatView(parent), window_(window), eventStream_(eventStream), fontSizeSteps_(0), disableAutoScroll_(disableAutoScroll), previousMessageKind_(PreviosuMessageWasNone), previousMessageWasSelf_(false), showEmoticons_(false), insertingLastLine_(false), idCounter_(0) { + theme_ = theme; + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(0); + mainLayout->setContentsMargins(0,0,0,0); + webView_ = new QtWebView(this); + connect(webView_, SIGNAL(linkClicked(const QUrl&)), SLOT(handleLinkClicked(const QUrl&))); + connect(webView_, SIGNAL(loadFinished(bool)), SLOT(handleViewLoadFinished(bool))); + connect(webView_, SIGNAL(gotFocus()), SIGNAL(gotFocus())); + connect(webView_, SIGNAL(clearRequested()), SLOT(handleClearRequested())); + connect(webView_, SIGNAL(fontGrowRequested()), SLOT(increaseFontSize())); + connect(webView_, SIGNAL(fontShrinkRequested()), SLOT(decreaseFontSize())); +#if defined (Q_OS_UNIX) && !defined(Q_OS_MAC) + /* To give a border on Linux, where it looks bad without */ + QStackedWidget* stack = new QStackedWidget(this); + stack->addWidget(webView_); + stack->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); + 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) { + 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)); +} + +void QtWebKitChatView::replaceLastMessage(const QString& newMessage, const QString& note) { + rememberScrolledToBottom(); + replaceLastMessage(newMessage); + 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::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))) { + //FIXME: Maybe do something here. Anything, really. + continue; + } + + } + return result; +} + + +QString QtWebKitChatView::getHighlightSpanStart(const HighlightAction& highlight) { + QString color = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextColor())); + QString background = QtUtilities::htmlEscape(P2QSTRING(highlight.getTextBackground())); + if (color.isEmpty()) { + color = "black"; + } + if (background.isEmpty()) { + background = "yellow"; + } + + return QString("<span style=\"color: %1; background: %2\">").arg(color).arg(background); +} + +std::string 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>" : ""; + 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)); +} + +// FIXME: Move this to a different file +std::string formatSize(const boost::uintmax_t bytes) { + static const char *siPrefix[] = {"k", "M", "G", "T", "P", "E", "Z", "Y", NULL}; + int power = 0; + double engBytes = bytes; + while (engBytes >= 1000) { + ++power; + engBytes = engBytes / 1000.0; + } + return str( boost::format("%.1lf %sB") % engBytes % (power > 0 ? siPrefix[power-1] : "") ); +} + +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(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>" : ""; + 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) { + replaceLastMessage(chatMessageToHTML(message)); +} + +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; + } + 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; +// } + + +} diff --git a/Swift/QtUI/QtWebKitChatView.h b/Swift/QtUI/QtWebKitChatView.h new file mode 100644 index 0000000..6bdaf96 --- /dev/null +++ b/Swift/QtUI/QtWebKitChatView.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2010-2013 Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <QString> +#include <QWidget> +#include <QList> +#include <QWebElement> + +#include <boost/shared_ptr.hpp> + +#include <Swiften/Base/Override.h> + +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> + +#include <Swift/QtUI/ChatSnippet.h> +#include <Swift/QtUI/QtChatView.h> + +class QWebPage; +class QUrl; +class QDate; + +namespace Swift { + class QtWebView; + class QtChatTheme; + class QtChatWindowJSBridge; + class UIEventStream; + class QtChatWindow; + class QtWebKitChatView : public QtChatView { + Q_OBJECT + + public: + static const QString ButtonWhiteboardSessionCancel; + static const QString ButtonWhiteboardSessionAcceptRequest; + static const QString ButtonWhiteboardShowWindow; + static const QString ButtonFileTransferCancel; + static const QString ButtonFileTransferSetDescription; + static const QString ButtonFileTransferSendRequest; + static const QString ButtonFileTransferAcceptRequest; + static const QString ButtonMUCInvite; + public: + QtWebKitChatView(QtChatWindow* window, UIEventStream* eventStream, QtChatTheme* theme, QWidget* parent, bool disableAutoScroll = false); + ~QtWebKitChatView(); + + /** Add message to window. + * @return id of added message (for acks). + */ + virtual std::string 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) SWIFTEN_OVERRIDE; + /** Adds action to window. + * @return id of added message (for acks); + */ + virtual std::string 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) SWIFTEN_OVERRIDE; + + virtual void addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) SWIFTEN_OVERRIDE; + virtual void addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction) SWIFTEN_OVERRIDE; + + virtual void addErrorMessage(const ChatWindow::ChatMessage& message) SWIFTEN_OVERRIDE; + virtual void replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) SWIFTEN_OVERRIDE; + virtual void replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& highlight) SWIFTEN_OVERRIDE; + void replaceLastMessage(const ChatWindow::ChatMessage& message); + void setAckState(const std::string& id, ChatWindow::AckState state); + + virtual std::string addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes) SWIFTEN_OVERRIDE; + virtual void setFileTransferProgress(std::string, const int percentageDone) SWIFTEN_OVERRIDE; + virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState state, const std::string& msg = "") SWIFTEN_OVERRIDE; + virtual void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) SWIFTEN_OVERRIDE; + virtual std::string addWhiteboardRequest(const QString& contact, bool senderIsSelf) SWIFTEN_OVERRIDE; + virtual void setWhiteboardSessionStatus(std::string id, const ChatWindow::WhiteboardSessionState state) SWIFTEN_OVERRIDE; + virtual void setMessageReceiptState(const std::string& id, ChatWindow::ReceiptState state) SWIFTEN_OVERRIDE; + + virtual void showEmoticons(bool show) SWIFTEN_OVERRIDE; + void addMessageTop(boost::shared_ptr<ChatSnippet> snippet); + void addMessageBottom(boost::shared_ptr<ChatSnippet> snippet); + + int getSnippetPositionByDate(const QDate& date); // FIXME : This probably shouldn't have been public + void addLastSeenLine(); + + private: // previously public, now private + void replaceLastMessage(const QString& newMessage); + void replaceLastMessage(const QString& newMessage, const QString& note); + void replaceMessage(const QString& newMessage, const QString& id, const QDateTime& time); + void rememberScrolledToBottom(); + void setAckXML(const QString& id, const QString& xml); + void setReceiptXML(const QString& id, const QString& xml); + void displayReceiptInfo(const QString& id, bool showIt); + + QString getLastSentMessage(); + void addToJSEnvironment(const QString&, QObject*); + void setFileTransferProgress(QString id, const int percentageDone); + void setFileTransferStatus(QString id, const ChatWindow::FileTransferState state, const QString& msg); + void setWhiteboardSessionStatus(QString id, const ChatWindow::WhiteboardSessionState state); + void setMUCInvitationJoined(QString id); + + signals: + void gotFocus(); + void fontResized(int); + void logCleared(); + void scrollRequested(int pos); + void scrollReachedTop(); + void scrollReachedBottom(); + + public slots: + void copySelectionToClipboard(); + void handleLinkClicked(const QUrl&); + void resetView(); + void resetTopInsertPoint(); + void increaseFontSize(int numSteps = 1); + void decreaseFontSize(); + void resizeFont(int fontSizeSteps); + void scrollToBottom(); + void handleKeyPressEvent(QKeyEvent* event); + + private slots: + void handleViewLoadFinished(bool); + void handleFrameSizeChanged(); + void handleClearRequested(); + void handleScrollRequested(int dx, int dy, const QRect& rectToScroll); + void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); + + private: + enum PreviousMessageKind { + PreviosuMessageWasNone, + PreviousMessageWasMessage, + PreviousMessageWasSystem, + PreviousMessageWasPresence, + PreviousMessageWasFileTransfer, + PreviousMessageWasMUCInvite + }; + std::string addMessage( + const QString& message, + const std::string& senderName, + bool senderIsSelf, + boost::shared_ptr<SecurityLabel> label, + const std::string& avatarPath, + const QString& style, + const boost::posix_time::ptime& time, + const HighlightAction& highlight, + ChatSnippet::Direction direction); + void replaceMessage( + const QString& message, + const std::string& id, + const boost::posix_time::ptime& time, + const QString& style, + const HighlightAction& highlight); + bool appendToPreviousCheck(PreviousMessageKind messageKind, const std::string& senderName, bool senderIsSelf); + static ChatSnippet::Direction getActualDirection(const ChatWindow::ChatMessage& message, ChatWindow::Direction direction); + QString chatMessageToHTML(const ChatWindow::ChatMessage& message); + QString getHighlightSpanStart(const HighlightAction& highlight); + static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString()); + + private: + void headerEncode(); + void messageEncode(); + void addToDOM(boost::shared_ptr<ChatSnippet> snippet); + QWebElement snippetToDOM(boost::shared_ptr<ChatSnippet> snippet); + + QtChatWindow* window_; + UIEventStream* eventStream_; + bool viewReady_; + bool isAtBottom_; + bool topMessageAdded_; + int scrollBarMaximum_; + QtWebView* webView_; + QWebPage* webPage_; + int fontSizeSteps_; + QtChatTheme* theme_; + QWebElement newInsertPoint_; + QWebElement topInsertPoint_; + QWebElement lineSeparator_; + QWebElement lastElement_; + QWebElement firstElement_; + QWebElement document_; + bool disableAutoScroll_; + QtChatWindowJSBridge* jsBridge; + PreviousMessageKind previousMessageKind_; + bool previousMessageWasSelf_; + bool showEmoticons_; + bool insertingLastLine_; + int idCounter_; + QString previousSenderName_; + std::map<QString, QString> descriptions_; + }; +} diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 86efb51..6835872 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -91,7 +91,6 @@ sources = [ "QtAvatarWidget.cpp", "QtUIFactory.cpp", "QtChatWindowFactory.cpp", - "QtChatWindow.cpp", "QtClickableLabel.cpp", "QtLoginWindow.cpp", "QtMainWindow.cpp", @@ -103,7 +102,9 @@ sources = [ "QtScaledAvatarCache.cpp", "QtSwift.cpp", "QtURIHandler.cpp", + "QtChatWindow.cpp", "QtChatView.cpp", + "QtWebKitChatView.cpp", "QtChatTheme.cpp", "QtChatTabs.cpp", "QtSoundPlayer.cpp", |