RetroShare/retroshare-gui/src/gui/chat/ChatWidget.cpp
2020-11-09 22:04:25 +01:00

1942 lines
61 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*******************************************************************************
* gui/chat/ChatWidget.cpp *
* *
* LibResAPI: API for local socket server *
* *
* Copyright (C) 2011, Retroshare Team <retroshare.project@gmail.com> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
#include <QApplication>
#include <QBuffer>
#include <QColorDialog>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QScrollBar>
#include <QStringListModel>
#include <QTextCodec>
#include <QTextDocumentFragment>
#include <QTextStream>
#include <QTimer>
#include <QToolTip>
#include <QInputDialog>
#include "ChatWidget.h"
#include "ui_ChatWidget.h"
#include "gui/MainWindow.h"
#include "gui/notifyqt.h"
#include "gui/RetroShareLink.h"
#include "gui/settings/rsharesettings.h"
#include "gui/settings/rsettingswin.h"
#include "gui/settings/RsharePeerSettings.h"
#include "gui/common/StatusDefs.h"
#include "gui/common/FilesDefs.h"
#include "gui/common/Emoticons.h"
#include "gui/chat/ChatLobbyDialog.h"
#include "gui/gxs/GxsIdDetails.h"
#include "util/misc.h"
#include "util/HandleRichText.h"
#include "gui/chat/ChatUserNotify.h"//For BradCast
#include "util/DateTime.h"
#include "util/imageutil.h"
#include "gui/im_history/ImHistoryBrowser.h"
#include <retroshare/rsstatus.h>
#include <retroshare/rsidentity.h>
#include <retroshare/rspeers.h>
#include <retroshare/rshistory.h>
#include <retroshare/rsmsgs.h>
#include <retroshare/rsplugin.h>
#include <time.h>
#define FMM 2.5//fontMetricsMultiplicator
#define FMM_SMALLER 1.8
#define FMM_THRESHOLD 25
/*****
* #define CHAT_DEBUG 1
*****/
ChatWidget::ChatWidget(QWidget *parent)
: QWidget(parent)
, completionPosition(0), newMessages(false), typing(false), peerStatus(0)
, sendingBlocked(false), useCMark(false)
, lastStatusSendTime(0)
, firstShow(true), inChatCharFormatChanged(false), firstSearch(true)
, lastUpdateCursorPos(0), lastUpdateCursorEnd(0)
, completer(NULL), imBrowser(NULL), notify(NULL)
, ui(new Ui::ChatWidget)
{
ui->setupUi(this);
int iconHeight = QFontMetricsF(font()).height();
double fmm = iconHeight > FMM_THRESHOLD ? FMM : FMM_SMALLER;
iconHeight *= fmm;
QSize iconSize = QSize(iconHeight, iconHeight);
//int butt_size(iconSize.height() + fmm);
//QSize buttonSize = QSize(butt_size, butt_size);
lastMsgDate = QDate::currentDate();
//Resize Tool buttons
//ui->emoteiconButton->setFixedSize(buttonSize);
ui->emoteiconButton->setIconSize(iconSize);
//ui->stickerButton->setFixedSize(buttonSize);
ui->stickerButton->setIconSize(iconSize);
//ui->attachPictureButton->setFixedSize(buttonSize);
ui->attachPictureButton->setIconSize(iconSize);
//ui->addFileButton->setFixedSize(buttonSize);
ui->addFileButton->setIconSize(iconSize);
//ui->pushtoolsButton->setFixedSize(buttonSize);
ui->pushtoolsButton->setIconSize(iconSize);
//ui->notifyButton->setFixedSize(buttonSize);
ui->notifyButton->setIconSize(iconSize);
//ui->markButton->setFixedSize(buttonSize);
ui->markButton->setIconSize(iconSize);
ui->leSearch->setFixedHeight(iconHeight);
ui->searchBefore->setFixedHeight(iconHeight);
ui->searchAfter->setFixedHeight(iconHeight);
//ui->searchButton->setFixedSize(buttonSize);
ui->searchButton->setIconSize(iconSize);
//ui->sendButton->setFixedHeight(iconHeight);
ui->sendButton->setIconSize(iconSize);
ui->typingLabel->setMaximumHeight(QFontMetricsF(font()).height()*1.2);
ui->fontcolorButton->setIconSize(iconSize);
//Initialize search
iCharToStartSearch=Settings->getChatSearchCharToStartSearch();
bFindCaseSensitively=Settings->getChatSearchCaseSensitively();
bFindWholeWords=Settings->getChatSearchWholeWords();
bMoveToCursor=Settings->getChatSearchMoveToCursor();
bSearchWithoutLimit=Settings->getChatSearchSearchWithoutLimit();
uiMaxSearchLimitColor=Settings->getChatSearchMaxSearchLimitColor();
cFoundColor=Settings->getChatSearchFoundColor();
ui->actionSearchWithoutLimit->setText(tr("Don't stop to color after")+" "+QString::number(uiMaxSearchLimitColor)+" "+tr("items found (need more CPU)"));
ui->markButton->setVisible(false);
ui->leSearch->setVisible(false);
ui->searchBefore->setVisible(false);
ui->searchBefore->setToolTip(tr("<b>Find Previous </b><br/><i>Ctrl+Shift+G</i>"));
ui->searchAfter->setVisible(false);
ui->searchAfter->setToolTip(tr("<b>Find Next </b><br/><i>Ctrl+G</i>"));
ui->searchButton->setCheckable(true);
ui->searchButton->setChecked(false);
ui->searchButton->setToolTip(tr("<b>Find </b><br/><i>Ctrl+F</i>"));
ui->leSearch->installEventFilter(this);
connect(ui->actionFindCaseSensitively, SIGNAL(triggered()), this, SLOT(toogle_FindCaseSensitively()));
connect(ui->actionFindWholeWords, SIGNAL(triggered()), this, SLOT(toogle_FindWholeWords()));
connect(ui->actionMoveToCursor, SIGNAL(triggered()), this, SLOT(toogle_MoveToCursor()));
connect(ui->actionSearchWithoutLimit, SIGNAL(triggered()), this, SLOT(toogle_SeachWithoutLimit()));
connect(ui->searchButton, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenuSearchButton(QPoint)));
ui->notifyButton->setVisible(false);
ui->markButton->setToolTip(tr("<b>Mark this selected text</b><br><i>Ctrl+M</i>"));
connect(ui->emoteiconButton, SIGNAL(clicked()), this, SLOT(smileyWidget()));
connect(ui->stickerButton, SIGNAL(clicked()), this, SLOT(stickerWidget()));
connect(ui->attachPictureButton, SIGNAL(clicked()), this, SLOT(addExtraPicture()));
connect(ui->addFileButton, SIGNAL(clicked()), this , SLOT(addExtraFile()));
connect(ui->sendButton, SIGNAL(clicked()), this, SLOT(sendChat()));
connect(ui->actionSaveChatHistory, SIGNAL(triggered()), this, SLOT(fileSaveAs()));
connect(ui->actionClearChatHistory, SIGNAL(triggered()), this, SLOT(clearChatHistory()));
connect(ui->actionDeleteChatHistory, SIGNAL(triggered()), this, SLOT(deleteChatHistory()));
connect(ui->actionMessageHistory, SIGNAL(triggered()), this, SLOT(messageHistory()));
connect(ui->actionChooseFont, SIGNAL(triggered()), this, SLOT(chooseFont()));
connect(ui->actionChooseColor, SIGNAL(triggered()), this, SLOT(chooseColor()));
connect(ui->actionResetFont, SIGNAL(triggered()), this, SLOT(resetFont()));
connect(ui->actionQuote, SIGNAL(triggered()), this, SLOT(quote()));
connect(ui->actionDropPlacemark, SIGNAL(triggered()), this, SLOT(dropPlacemark()));
connect(ui->actionSave_image, SIGNAL(triggered()), this, SLOT(saveImage()));
connect(ui->actionImport_sticker, SIGNAL(triggered()), this, SLOT(saveSticker()));
connect(ui->actionShow_Hidden_Images, SIGNAL(triggered()), ui->textBrowser, SLOT(showImages()));
ui->actionShow_Hidden_Images->setIcon(ui->textBrowser->getBlockedImage());
connect(ui->hashBox, SIGNAL(fileHashingFinished(QList<HashedFile>)), this, SLOT(fileHashingFinished(QList<HashedFile>)));
connect(NotifyQt::getInstance(), SIGNAL(peerStatusChanged(const QString&, int)), this, SLOT(updateStatus(const QString&, int)));
connect(NotifyQt::getInstance(), SIGNAL(peerHasNewCustomStateString(const QString&, const QString&)), this, SLOT(updatePeersCustomStateString(const QString&, const QString&)));
connect(NotifyQt::getInstance(), SIGNAL(chatFontChanged()), this, SLOT(resetFonts()));
connect(ui->textBrowser, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenuTextBrowser(QPoint)));
//connect(ui->chatTextEdit, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenu(QPoint)));
// reset text and color after removing all characters from the QTextEdit and after calling QTextEdit::clear
connect(ui->chatTextEdit, SIGNAL(currentCharFormatChanged(QTextCharFormat)), this, SLOT(chatCharFormatChanged()));
connect(ui->chatTextEdit, SIGNAL(textChanged()), this, SLOT(updateLenOfChatTextEdit()));
ui->infoFrame->setVisible(false);
ui->statusMessageLabel->hide();
setAcceptDrops(true);
ui->chatTextEdit->setAcceptDrops(false);
ui->hashBox->setDropWidget(this);
ui->hashBox->setAutoHide(true);
QMenu *fontmenu = new QMenu();
fontmenu->addAction(ui->actionChooseFont);
fontmenu->addAction(ui->actionChooseColor);
fontmenu->addAction(ui->actionResetFont);
fontmenu->addAction(ui->actionNoEmbed);
fontmenu->addAction(ui->actionSendAsPlainText);
#ifdef USE_CMARK
fontmenu->addAction(ui->actionSend_as_CommonMark);
#endif
ui->fontcolorButton->setMenu(fontmenu);
QMenu *menu = new QMenu();
menu->addAction(ui->actionMessageHistory);
menu->addSeparator();
menu->addAction(ui->actionSaveChatHistory);
menu->addAction(ui->actionClearChatHistory);
menu->addAction(ui->actionDeleteChatHistory);
ui->pushtoolsButton->setMenu(menu);
ui->actionSendAsPlainText->setChecked(Settings->getChatSendAsPlainTextByDef());
ui->chatTextEdit->setOnlyPlainText(ui->actionSendAsPlainText->isChecked());
connect(ui->actionSendAsPlainText, SIGNAL(toggled(bool)), ui->chatTextEdit, SLOT(setOnlyPlainText(bool)) );
#ifdef USE_CMARK
connect(ui->actionSend_as_CommonMark, SIGNAL(toggled(bool)), this, SLOT(setUseCMark(bool)) );
connect(ui->chatTextEdit, SIGNAL(textChanged()), this, SLOT(updateCMPreview()) );
#endif
ui->cmPreview->setVisible(false);
ui->textBrowser->resetImagesStatus(Settings->getChatLoadEmbeddedImages());
ui->textBrowser->installEventFilter(this);
ui->textBrowser->viewport()->installEventFilter(this);
ui->chatTextEdit->installEventFilter(this);
//ui->textBrowser->setMouseTracking(true);
//ui->chatTextEdit->setMouseTracking(true);
#if QT_VERSION < 0x040700
// embedded images are not supported before QT 4.7.0
ui->attachPictureButton->setVisible(false);
#endif
resetStatusBar();
completer = new QCompleter(this);
//completer->setModel(modelFromPeers()); //No peers at this point.
completer->setModelSorting(QCompleter::UnsortedModel);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setWrapAround(false);
ui->chatTextEdit->setCompleter(completer);
ui->chatTextEdit->setCompleterKeyModifiers(Qt::ControlModifier);
ui->chatTextEdit->setCompleterKey(Qt::Key_Space);
//#ifdef ENABLE_DISTANT_CHAT_AND_MSGS
// contextMnu->addSeparator();
// QAction *action = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/pasterslink.png"), tr("Paste/Create private chat or Message link..."), this);
// connect(action, SIGNAL(triggered()), this, SLOT(pasteCreateMsgLink()));
// ui->chatTextEdit->addContextMenuAction(action);
//#endif
}
ChatWidget::~ChatWidget()
{
processSettings(false);
/* Cleanup plugin functions */
foreach (ChatWidgetHolder *chatWidgetHolder, mChatWidgetHolder) {
delete(chatWidgetHolder);
}
delete ui;
}
void ChatWidget::setDefaultExtraFileFlags(TransferRequestFlags fl)
{
mDefaultExtraFileFlags = fl ;
ui->hashBox->setDefaultTransferRequestFlags(fl) ;
}
void ChatWidget::addChatHorizontalWidget(QWidget *w)
{
ui->pluginsVLayout->addWidget(w) ;
update() ;
}
void ChatWidget::addChatBarWidget(QWidget *w)
{
int iconHeight = QFontMetricsF(font()).height();
double fmm = iconHeight > FMM_THRESHOLD ? FMM : FMM_SMALLER;
iconHeight *= fmm;
QSize iconSize = QSize(iconHeight, iconHeight);
int butt_size(iconSize.height() + fmm);
QSize buttonSize = QSize(butt_size, butt_size);
w->setFixedSize(buttonSize);
ui->pluginButtonFrame->layout()->addWidget(w) ;
}
void ChatWidget::addTitleBarWidget(QWidget *w)
{
ui->pluginTitleFrame->layout()->addWidget(w) ;
}
void ChatWidget::hideChatText(bool hidden)
{
ui->chatTextFrame->setHidden(hidden); ;
}
RSButtonOnText* ChatWidget::getNewButtonOnTextBrowser()
{
return new RSButtonOnText(ui->textBrowser);
}
RSButtonOnText* ChatWidget::getNewButtonOnTextBrowser(QString text)
{
return new RSButtonOnText(text, ui->textBrowser);
}
void ChatWidget::init(const ChatId &chat_id, const QString &title)
{
this->chatId = chat_id;
this->title = title;
ui->titleLabel->setText(RsHtml::plainText(title));
ui->chatTextEdit->setMaxBytes(this->maxMessageSize() - 200);
RsPeerId ownId = rsPeers->getOwnId();
setName(QString::fromUtf8(rsPeers->getPeerName(ownId).c_str()));
if(chatId.isPeerId() || chatId.isDistantChatId())
chatStyle.setStyleFromSettings(ChatStyle::TYPE_PRIVATE);
if(chatId.isBroadcast() || chatId.isLobbyId())
chatStyle.setStyleFromSettings(ChatStyle::TYPE_PUBLIC);
currentColor.setNamedColor(PeerSettings->getPrivateChatColor(chatId));
currentFont.fromString(PeerSettings->getPrivateChatFont(chatId));
colorChanged();
setColorAndFont(true);
// load style
PeerSettings->getStyle(chatId, "ChatWidget", style);
/* Add plugin functions */
int pluginCount = rsPlugins->nbPlugins();
for (int i = 0; i < pluginCount; ++i) {
RsPlugin *plugin = rsPlugins->plugin(i);
if (plugin) {
ChatWidgetHolder *chatWidgetHolder = plugin->qt_get_chat_widget_holder(this);
if (chatWidgetHolder) {
mChatWidgetHolder.push_back(chatWidgetHolder);
}
}
}
uint32_t hist_chat_type = 0xFFFF; // a value larger than the biggest RS_HISTORY_TYPE_* value
int messageCount=0;
if (chatType() == CHATTYPE_LOBBY) {
hist_chat_type = RS_HISTORY_TYPE_LOBBY;
messageCount = Settings->getLobbyChatHistoryCount();
ui->statusLabel->hide();
updateTitle();
} else if (chatType() == CHATTYPE_PRIVATE){
hist_chat_type = RS_HISTORY_TYPE_PRIVATE ;
messageCount = Settings->getPrivateChatHistoryCount();
// initialize first status
StatusInfo peerStatusInfo;
// No check of return value. Non existing status info is handled as offline.
rsStatus->getStatus(chatId.toPeerId(), peerStatusInfo);
updateStatus(QString::fromStdString(chatId.toPeerId().toStdString()), peerStatusInfo.status);
// initialize first custom state string
QString customStateString = QString::fromUtf8(rsMsgs->getCustomStateString(chatId.toPeerId()).c_str());
updatePeersCustomStateString(QString::fromStdString(chatId.toPeerId().toStdString()), customStateString);
} else if (chatType() == CHATTYPE_DISTANT){
hist_chat_type = RS_HISTORY_TYPE_DISTANT ;
messageCount = Settings->getDistantChatHistoryCount();
} else if(chatId.isBroadcast()){
hist_chat_type = RS_HISTORY_TYPE_PUBLIC;
messageCount = Settings->getPublicChatHistoryCount();
ui->titleBarFrame->setVisible(false);
}
if (rsHistory->getEnable(hist_chat_type))
{
// get chat messages from history
std::list<HistoryMsg> historyMsgs;
if (messageCount > 0)
{
rsHistory->getMessages(chatId, historyMsgs, messageCount);
std::list<HistoryMsg>::iterator historyIt;
for (historyIt = historyMsgs.begin(); historyIt != historyMsgs.end(); ++historyIt)
{
// it can happen that a message is first added to the message history
// and later the gui receives the message through notify
// avoid this by not adding history entries if their age is < 2secs
if (time(nullptr) <= historyIt->recvTime+2)
continue;
QString name;
if (chatId.isLobbyId() || chatId.isDistantChatId())
{
RsIdentityDetails details;
time_t start = time(nullptr);
while (!rsIdentity->getIdDetails(RsGxsId(historyIt->peerName), details))
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (time(nullptr)>start+2)
{
std::cerr << "ChatWidget History haven't found Id Details and have wait 1 sec for it." << std::endl;
break;
}
}
if (rsIdentity->getIdDetails(RsGxsId(historyIt->peerName), details))
name = QString::fromUtf8(details.mNickname.c_str());
else
name = QString::fromUtf8(historyIt->peerName.c_str());
} else {
name = QString::fromUtf8(historyIt->peerName.c_str());
}
addChatMsg(historyIt->incoming, name, RsGxsId(historyIt->peerName.c_str()), QDateTime::fromTime_t(historyIt->sendTime), QDateTime::fromTime_t(historyIt->recvTime), QString::fromUtf8(historyIt->message.c_str()), MSGTYPE_HISTORY);
}
}
}
processSettings(true);
}
ChatId ChatWidget::getChatId()
{
return chatId;
}
ChatWidget::ChatType ChatWidget::chatType()
{
// transformation from ChatId::Type to ChatWidget::ChatType
// we don't use the type in ChatId directly, because of historic reasons
// ChatWidget::ChatType existed before ChatId::Type was introduced
// TODO: check if can change all code to use the type in ChatId directly
// but maybe it is good to have separate types in libretroshare and gui
if(chatId.isPeerId())
return CHATTYPE_PRIVATE;
if(chatId.isDistantChatId())
return CHATTYPE_DISTANT;
if(chatId.isLobbyId())
return CHATTYPE_LOBBY;
return CHATTYPE_UNKNOWN;
}
void ChatWidget::blockSending(QString msg)
{
#ifndef RS_ASYNC_CHAT
// sendingBlocked = true;
// ui->sendButton->setEnabled(false);
// ui->stickerButton->setEnabled(false);
#endif
ui->sendButton->setToolTip(msg);
}
void ChatWidget::unblockSending()
{
sendingBlocked = false;
ui->stickerButton->setEnabled(true);
updateLenOfChatTextEdit();
}
void ChatWidget::processSettings(bool load)
{
Settings->beginGroup(QString("ChatWidget"));
if (load) {
// load settings
// state of splitter
ui->chatVSplitter->restoreState(Settings->value("ChatSplitter").toByteArray());
} else {
// save settings
// state of splitter
Settings->setValue("ChatSplitter", ui->chatVSplitter->saveState());
}
Settings->endGroup();
}
uint32_t ChatWidget::maxMessageSize()
{
uint32_t maxMessageSize = 0;
switch (chatType()) {
case CHATTYPE_UNKNOWN:
break;
case CHATTYPE_PRIVATE:
maxMessageSize = rsMsgs->getMaxMessageSecuritySize(RS_CHAT_TYPE_PRIVATE);
break;
case CHATTYPE_LOBBY:
maxMessageSize = rsMsgs->getMaxMessageSecuritySize(RS_CHAT_TYPE_LOBBY);
break;
case CHATTYPE_DISTANT:
maxMessageSize = rsMsgs->getMaxMessageSecuritySize(RS_CHAT_TYPE_DISTANT);
break;
}
return maxMessageSize;
}
bool ChatWidget::eventFilter(QObject *obj, QEvent *event)
{
//QEvent::Type type = event->type();
if (obj == ui->textBrowser || obj == ui->textBrowser->viewport()
|| obj == ui->leSearch || obj == ui->chatTextEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent) {
if (keyEvent->key() == Qt::Key_F && keyEvent->modifiers() == Qt::ControlModifier)
{
bool bTextselected=false;
if (obj == ui->textBrowser )
{
if (ui->textBrowser->textCursor().selectedText().length()>0)
{
ui->leSearch->setText(ui->textBrowser->textCursor().selectedText());
bTextselected=true;
}
}
if (obj == ui->chatTextEdit)
{
if (ui->chatTextEdit->textCursor().selectedText().length()>0)
{
ui->leSearch->setText(ui->chatTextEdit->textCursor().selectedText());
bTextselected=true;
}
}
ui->searchButton->setChecked(!ui->searchButton->isChecked() || bTextselected);
ui->leSearch->setVisible(bTextselected);//To discard re-selection of text
on_searchButton_clicked(ui->searchButton->isChecked());
return true; // eat event
}
if (keyEvent->key() == Qt::Key_G && keyEvent->modifiers() == Qt::ControlModifier)
{
if (ui->searchAfter->isVisible())
on_searchAfter_clicked();
return true; // eat event
}
if (keyEvent->key() == Qt::Key_G && keyEvent->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier))
{
if (ui->searchBefore->isVisible())
on_searchBefore_clicked();
return true; // eat event
}
}
}
if (notify && chatType() == CHATTYPE_LOBBY) {
if ((event->type() == QEvent::KeyPress)
|| (event->type() == QEvent::MouseMove)
|| (event->type() == QEvent::Enter)
|| (event->type() == QEvent::Leave)
|| (event->type() == QEvent::Wheel)
|| (event->type() == QEvent::ToolTip) ) {
QTextCursor cursor = ui->textBrowser->cursorForPosition(QPoint(0, 0));
QPoint bottom_right(ui->textBrowser->viewport()->width() - 1, ui->textBrowser->viewport()->height() - 1);
int end_pos = ui->textBrowser->cursorForPosition(bottom_right).position();
cursor.setPosition(end_pos, QTextCursor::KeepAnchor);
if ((cursor.position() != lastUpdateCursorPos || cursor.selectionEnd() != lastUpdateCursorEnd) &&
!cursor.selectedText().isEmpty()) {
lastUpdateCursorPos = cursor.position();
lastUpdateCursorEnd = cursor.selectionEnd();
QRegExp rx("<a name=\"(.*)\"",Qt::CaseSensitive, QRegExp::RegExp2);
rx.setMinimal(true);
QString sel=cursor.selection().toHtml();
QStringList anchors;
int pos=0;
while ((pos = rx.indexIn(sel,pos)) != -1) {
anchors << rx.cap(1);
pos += rx.matchedLength();
}
if (!anchors.isEmpty()){
for (QStringList::iterator it=anchors.begin();it!=anchors.end();++it) {
notify->chatLobbyCleared(chatId.toLobbyId(), *it);
}
}
}
}
}
}
if (obj == ui->textBrowser) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent) {
if (keyEvent->key() == Qt::Key_Delete) {
// Delete key pressed
if (ui->textBrowser->textCursor().selectedText().length() > 0) {
if (notify && chatType() == CHATTYPE_LOBBY) {
QRegExp rx("<a name=\"(.*)\"",Qt::CaseSensitive, QRegExp::RegExp2);
rx.setMinimal(true);
QString sel=ui->textBrowser->textCursor().selection().toHtml();
QStringList anchors;
int pos=0;
while ((pos = rx.indexIn(sel,pos)) != -1) {
anchors << rx.cap(1);
pos += rx.matchedLength();
}
for (QStringList::iterator it=anchors.begin();it!=anchors.end();++it) {
notify->chatLobbyCleared(chatId.toLobbyId(), *it);
}
}
ui->textBrowser->textCursor().deleteChar();
}
}
if (keyEvent->key() == Qt::Key_M && keyEvent->modifiers() == Qt::ControlModifier)
{
on_markButton_clicked(!ui->markButton->isChecked());
}
}
}
if (event->type() == QEvent::ToolTip) {
QHelpEvent* helpEvent = static_cast<QHelpEvent*>(event);
QString toolTipText = ui->textBrowser->anchorForPosition(helpEvent->pos());
if (toolTipText.isEmpty() && !ui->textBrowser->getShowImages()){
QString imageStr;
if (ui->textBrowser->checkImage(helpEvent->pos(), imageStr)) {
toolTipText = imageStr;
}
} else if (toolTipText.startsWith(PERSONID)){
toolTipText = toolTipText.replace(PERSONID, tr("Person id: ") );
toolTipText = toolTipText.append(tr("\nDouble click on it to add his name on text writer.") );
}
if (!toolTipText.isEmpty()){
QToolTip::showText(helpEvent->globalPos(), toolTipText);
return true;
} else {
QToolTip::hideText();
}
}
} else if (obj == ui->chatTextEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent) {
if (!keyEvent->text().isEmpty()) {
updateStatusTyping();
}
if (chatType() == CHATTYPE_LOBBY) {
if (keyEvent->key() == Qt::Key_Tab) {
completeNickname((bool)(keyEvent->modifiers() & Qt::ShiftModifier));
return true; // eat event
}
else {
completionWord.clear();
}
if ((keyEvent->modifiers() & ui->chatTextEdit->getCompleterKeyModifiers()) && keyEvent->key() == ui->chatTextEdit->getCompleterKey()) {
completer->setModel(modelFromPeers());
}
if (keyEvent->text()=="@") {
ui->chatTextEdit->forceCompleterShowNextKeyEvent("@");
completer->setModel(modelFromPeers());
}
}
if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
// Enter pressed
if (Settings->getChatSendMessageWithCtrlReturn()) {
if (keyEvent->modifiers() & Qt::ControlModifier) {
// send message with Ctrl+Enter
sendChat();
return true; // eat event
}
} else {
if ((keyEvent->modifiers() & Qt::ControlModifier) || (keyEvent->modifiers() & Qt::ShiftModifier)){
// insert return
ui->chatTextEdit->textCursor().insertText("\n");
} else {
// send message with Enter
sendChat();
}
return true; // eat event
}
}
}
}
if (event->type() == QEvent::StyleChange)
{
QString colorName = currentColor.name();
qreal desiredContrast = Settings->valueFromGroup("Chat", "MinimumContrast", 4.5).toDouble();
QColor backgroundColor = ui->chatTextEdit->palette().base().color();
RsHtml::findBestColor(colorName, backgroundColor, desiredContrast);
currentColor = QColor(colorName);
ui->chatTextEdit->setTextColor(currentColor);
colorChanged();
}
} else if (obj == ui->leSearch) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent) {
QString qsTextToFind=ui->leSearch->text();
if (keyEvent->key()==Qt::Key_Backspace) {
qsTextToFind=qsTextToFind.left(qsTextToFind.length()-1);// "\010"
} else if (keyEvent->key()==Qt::Key_Tab) { // "\011"
} else if (keyEvent->key()==Qt::Key_Return) { // "\015"
} else if (keyEvent->text().length()==1)
qsTextToFind+=keyEvent->text();
if (((qsTextToFind.length()>=iCharToStartSearch) || (keyEvent->key()==Qt::Key_Return)) && (keyEvent->text().length()>0))
{
findText(qsTextToFind);
} else {
ui->leSearch->setPalette(qpSave_leSearch);
}
}
}
} else if (obj == ui->textBrowser->viewport()) {
if (event->type() == QEvent::MouseButtonDblClick) {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
QString anchor = ui->textBrowser->anchorForPosition(mouseEvent->pos());
if (!anchor.isEmpty()){
if (anchor.startsWith(PERSONID)){
QString strId = anchor.replace(PERSONID,"");
if (strId.contains(" "))
strId.truncate(strId.indexOf(" "));
RsGxsId mId = RsGxsId(strId.toStdString());
if(!mId.isNull()) {
RsIdentityDetails details;
if (rsIdentity->getIdDetails(mId, details)){
QString text = QString("@").append(GxsIdDetails::getName(details)).append(" ");
ui->chatTextEdit->textCursor().insertText(text);
}
}
}
}
}
} else {
if (event->type() == QEvent::WindowActivate) {
if (isVisible() && (window() == NULL || window()->isActiveWindow())) {
newMessages = false;
emit infoChanged(this);
}
}
}
// pass the event on to the parent class
return QWidget::eventFilter(obj, event);
}
/**
* @brief Utility function for completeNickname.
*/
static bool caseInsensitiveCompare(QString a, QString b)
{
return a.toLower() < b.toLower();
}
/**
* @brief Completes nickname based on previous characters.
* @param reverse true to list nicknames in reverse alphabetical order.
*/
void ChatWidget::completeNickname(bool reverse)
{
// Find lobby we belong to
ChatLobbyInfo lobby;
if (! rsMsgs->getChatLobbyInfo(chatId.toLobbyId(),lobby))
return;
QTextCursor cursor = ui->chatTextEdit->textCursor();
// Do nothing if there is a selection
if (cursor.anchor() != cursor.position())
return;
// We want to complete the word typed by the user. This is easy.
// But also, if there are two participants whose name start with the word
// the user typed, the first press on <tab> should show the first
// participants name, and the second press on <tab> should show the
// second participants name.
cursor.beginEditBlock();
if (!completionWord.isEmpty()) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, cursor.position() - completionPosition);
}
else {
cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
completionPosition = cursor.position();
}
if (cursor.selectedText() == ": ") {
cursor.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
}
bool firstWord = (cursor.position() == 0);
QString word = cursor.selectedText();
if (word.endsWith(": ")) {
word.chop(2);
}
if (word.length() > 0) {
// Sort participants list
std::list<QString> participants;
RsIdentityDetails details ;
for (auto it = lobby.gxs_ids.begin(); it != lobby.gxs_ids.end(); ++it)
{
if(rsIdentity->getIdDetails(it->first,details))
participants.push_front(QString::fromUtf8(details.mNickname.c_str()));
}
participants.sort(caseInsensitiveCompare);
// Search for a participant nickname that starts with the previous word
std::list<QString>::const_iterator it, start, end;
int delta;
bool skippedParticipant = false;
if (reverse) {
// Note: at the moment reverse completion doesnt work because
// shift-tab selects the previous widget
start = participants.end();
--start;
end = participants.begin();
--end;
delta = -1;
}
else {
start = participants.begin();
end = participants.end();
delta = 1;
}
do {
for (it = start; it != end; (delta == 1) ? ++it : --it) {
QString participant = *it;
if (participant.startsWith(word, Qt::CaseInsensitive)) {
if (!completionWord.isEmpty() && !skippedParticipant) {
// This participant nicknaem was completed with <tab>;
// skip it and find the next one that starts with
// completionWord
word = completionWord;
skippedParticipant = true;
continue;
}
skippedParticipant = false;
cursor.insertText(participant);
if (firstWord) {
cursor.insertText(QString(": "));
}
break;
}
}
} while (skippedParticipant);
if (completionWord.isEmpty()) {
completionWord = word;
}
}
cursor.endEditBlock();
}
QAbstractItemModel *ChatWidget::modelFromPeers()
{
// Find lobby we belong to
ChatLobbyInfo lobby ;
if(! rsMsgs->getChatLobbyInfo(chatId.toLobbyId(),lobby))
return new QStringListModel(completer);
#ifndef QT_NO_CURSOR
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
#endif
// Get participants list
QStringList participants;
for (auto it = lobby.gxs_ids.begin(); it != lobby.gxs_ids.end(); ++it)
{
RsIdentityDetails details ;
rsIdentity->getIdDetails(it->first,details) ;
participants.push_front(QString::fromUtf8(details.mNickname.c_str()));
}
#ifndef QT_NO_CURSOR
QApplication::restoreOverrideCursor();
#endif
return new QStringListModel(participants, completer);
}
void ChatWidget::addToolsAction(QAction *action)
{
ui->pushtoolsButton->menu()->addAction(action);
}
void ChatWidget::showEvent(QShowEvent */*event*/)
{
newMessages = false;
emit infoChanged(this);
// if user waded through the jungle of history just let him on
// own decide whether to continue the journey or start typing
QScrollBar *scrollbar = ui->textBrowser->verticalScrollBar();
bool is_scrollbar_at_end = scrollbar->value() == scrollbar->maximum();
bool is_chat_text_edit_empty = ui->chatTextEdit->toPlainText().isEmpty();
if (is_scrollbar_at_end || !is_chat_text_edit_empty) {
focusDialog();
} else {
// otherwise focus will be get even not chat itself
ui->textBrowser->setFocus();
}
ChatUserNotify::clearWaitingChat(chatId);
if (firstShow) {
// Workaround: now the scroll position is correct calculated
firstShow = false;
QScrollBar *scrollbar = ui->textBrowser->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
}
void ChatWidget::resizeEvent(QResizeEvent */*event*/)
{
// Workaround: now the scroll position is correct calculated
QScrollBar *scrollbar = ui->textBrowser->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
void ChatWidget::addToParent(QWidget *newParent)
{
newParent->window()->installEventFilter(this);
}
void ChatWidget::removeFromParent(QWidget *oldParent)
{
oldParent->window()->removeEventFilter(this);
}
void ChatWidget::focusDialog()
{
ui->chatTextEdit->setFocus();
}
QToolButton* ChatWidget::getNotifyButton()
{
if (ui) if (ui->notifyButton) return ui->notifyButton;
return NULL;
}
void ChatWidget::setNotify(ChatLobbyUserNotify *clun)
{
if(clun) notify=clun;
}
void ChatWidget::on_notifyButton_clicked()
{
if(!notify) return;
if (chatType() != CHATTYPE_LOBBY) return;
QMenu* menu = new QMenu(MainWindow::getInstance());
QIcon icoLobby=(ui->notifyButton->icon());
notify->makeSubMenu(menu, icoLobby, title, chatId.toLobbyId());
menu->exec(ui->notifyButton->mapToGlobal(QPoint(0,ui->notifyButton->geometry().height())));
}
void ChatWidget::scrollToAnchor(QString anchor)
{
ui->textBrowser->scrollToAnchor(anchor);
}
void ChatWidget::setWelcomeMessage(QString &text)
{
ui->textBrowser->setText(text);
}
void ChatWidget::addChatMsg(bool incoming, const QString &name, const QDateTime &sendTime, const QDateTime &recvTime, const QString &message, MsgType chatType)
{
addChatMsg(incoming, name, RsGxsId(), sendTime, recvTime, message, chatType);
}
void ChatWidget::addChatMsg(bool incoming, const QString &name, const RsGxsId gxsId
, const QDateTime &sendTime, const QDateTime &recvTime
, const QString &message, MsgType chatType)
{
#ifdef CHAT_DEBUG
std::cout << "ChatWidget::addChatMsg message : " << message.toStdString() << std::endl;
#endif
unsigned int formatTextFlag = RSHTML_FORMATTEXT_EMBED_LINKS | RSHTML_FORMATTEXT_OPTIMIZE;
unsigned int formatFlag = 0;
bool addDate = false;
if (QDate::currentDate()>lastMsgDate)
{
addDate=true;
}
// embed smileys ?
if (Settings->valueFromGroup(QString("Chat"), QString::fromUtf8("Emoteicons_PrivatChat"), true).toBool()) {
if (!message.contains("NoEmbed=\"true\""))
formatTextFlag |= RSHTML_FORMATTEXT_EMBED_SMILEYS;
}
#ifdef USE_CMARK
//Use CommonMark
if (message.contains("CMark=\"true\"")) {
formatTextFlag |= RSHTML_FORMATTEXT_USE_CMARK;
}
#endif
// Always fix colors
formatTextFlag |= RSHTML_FORMATTEXT_FIX_COLORS;
qreal desiredContrast = Settings->valueFromGroup("Chat", "MinimumContrast", 4.5).toDouble();
QColor backgroundColor = ui->textBrowser->palette().base().color();
// Remove font name, size, bold, italics?
if (!Settings->valueFromGroup("Chat", "EnableCustomFonts", true).toBool()) {
formatTextFlag |= RSHTML_FORMATTEXT_REMOVE_FONT_FAMILY;
}
if (!Settings->valueFromGroup("Chat", "EnableCustomFontSize", true).toBool()) {
formatTextFlag |= RSHTML_FORMATTEXT_REMOVE_FONT_SIZE;
}
int desiredMinimumFontSize = Settings->valueFromGroup("Chat", "MinimumFontSize", 10).toInt();
if (!Settings->valueFromGroup("Chat", "EnableBold", true).toBool()) {
formatTextFlag |= RSHTML_FORMATTEXT_REMOVE_FONT_WEIGHT;
}
if (!Settings->valueFromGroup("Chat", "EnableItalics", true).toBool()) {
formatTextFlag |= RSHTML_FORMATTEXT_REMOVE_FONT_STYLE;
}
ChatStyle::enumFormatMessage type;
if (chatType == MSGTYPE_OFFLINE) {
type = ChatStyle::FORMATMSG_OOUTGOING;
} else if (chatType == MSGTYPE_SYSTEM) {
type = ChatStyle::FORMATMSG_SYSTEM;
} else if (chatType == MSGTYPE_HISTORY || addDate) {
lastMsgDate=QDate::currentDate();
type = incoming ? ChatStyle::FORMATMSG_HINCOMING : ChatStyle::FORMATMSG_HOUTGOING;
} else {
type = incoming ? ChatStyle::FORMATMSG_INCOMING : ChatStyle::FORMATMSG_OUTGOING;
}
if (chatType == MSGTYPE_SYSTEM) {
formatFlag |= CHAT_FORMATMSG_SYSTEM;
}
QString formattedMessage = RsHtml().formatText(ui->textBrowser->document(), message, formatTextFlag, backgroundColor, desiredContrast, desiredMinimumFontSize);
QDateTime dtTimestamp=incoming ? sendTime : recvTime;
QString formatMsg = chatStyle.formatMessage(type, name, dtTimestamp, formattedMessage, formatFlag, backgroundColor);
QString timeStamp = dtTimestamp.toString(Qt::ISODate);
//replace Date and Time anchors
formatMsg.replace(QString("<a name=\"date\">"),QString("<a name=\"%1\">").arg(timeStamp));
formatMsg.replace(QString("<a name=\"time\">"),QString("<a name=\"%1\">").arg(timeStamp));
//replace Name anchors with GXS Id
if (!gxsId.isNull()) {
RsIdentityDetails details;
QString strPreName = "";
QString strGxsId = QString::fromStdString(gxsId.toStdString());
rsIdentity->getIdDetails(gxsId, details);
bool isUnsigned = !(details.mFlags & RS_IDENTITY_FLAGS_PGP_LINKED);
if(isUnsigned && ui->textBrowser->getShowImages()) {
QIcon icon = FilesDefs::getIconFromQtResourcePath(":/icons/anonymous_blue_128.png");
int height = ui->textBrowser->fontMetrics().height()*0.8;
QImage image(icon.pixmap(height,height).toImage());
QByteArray byteArray;
QBuffer buffer(&byteArray);
image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer
QString iconBase64 = QString::fromLatin1(byteArray.toBase64().data());
strPreName = QString("<img src=\"data:image/png;base64,%1\" alt=\"[unsigned]\" />").arg(iconBase64);
}
formatMsg.replace(QString("<a name=\"name\">")
,QString(strPreName).append("<a name=\"").append(PERSONID).append("%1 %2\">").arg(strGxsId, isUnsigned ? tr(" Unsigned"):""));
} else {
formatMsg.replace(QString("<a name=\"name\">"),"");
}
QTextCursor textCursor = QTextCursor(ui->textBrowser->textCursor());
textCursor.movePosition(QTextCursor::End);
textCursor.setBlockFormat(QTextBlockFormat ());
ui->textBrowser->append(formatMsg);
if (ui->leSearch->isVisible()) {
QString qsTextToFind=ui->leSearch->text();
findText(qsTextToFind);
}
resetStatusBar();
if (incoming && chatType == MSGTYPE_NORMAL) {
emit newMessage(this);
if (!isActive()) {
newMessages = true;
}
emit infoChanged(this);
}
}
bool ChatWidget::isActive()
{
if (!isVisible() || (window() && (!window()->isActiveWindow() || window()->isMinimized()))) {
return false;
}
return true;
}
void ChatWidget::pasteText(const QString& S)
{
//std::cerr << "In paste link" << std::endl;
ui->chatTextEdit->insertHtml(S);
setColorAndFont(false);
}
//void ChatWidget::pasteCreateMsgLink()
//{
// RSettingsWin::showYourself(this, RSettingsWin::Chat);
//}
void ChatWidget::contextMenuTextBrowser(QPoint point)
{
QMatrix matrix;
matrix.translate(ui->textBrowser->horizontalScrollBar()->value(), ui->textBrowser->verticalScrollBar()->value());
QMenu *contextMnu = ui->textBrowser->createStandardContextMenu(matrix.map(point));
contextMnu->addSeparator();
contextMnu->addAction(ui->actionClearChatHistory);
if (ui->textBrowser->textCursor().selection().toPlainText().length())
contextMnu->addAction(ui->actionQuote);
contextMnu->addAction(ui->actionDropPlacemark);
if(ui->textBrowser->checkImage(point))
{
if (! ui->textBrowser->getShowImages())
contextMnu->addAction(ui->actionShow_Hidden_Images);
ui->actionSave_image->setData(point);
ui->actionImport_sticker->setData(point);
contextMnu->addAction(ui->actionSave_image);
contextMnu->addAction(ui->actionImport_sticker);
}
QString anchor = ui->textBrowser->anchorForPosition(point);
emit textBrowserAskContextMenu(contextMnu, anchor, point);
contextMnu->exec(ui->textBrowser->viewport()->mapToGlobal(point));
delete(contextMnu);
}
void ChatWidget::contextMenuSearchButton(QPoint /*point*/)
{
QMenu *contextMnu = new QMenu;
contextMnu->addSeparator();
ui->actionFindCaseSensitively->setChecked(bFindCaseSensitively);
contextMnu->addAction(ui->actionFindCaseSensitively);
ui->actionFindWholeWords->setChecked(bFindWholeWords);
contextMnu->addAction(ui->actionFindWholeWords);
ui->actionMoveToCursor->setChecked(bMoveToCursor);
contextMnu->addAction(ui->actionMoveToCursor);
ui->actionSearchWithoutLimit->setChecked(bSearchWithoutLimit);
contextMnu->addAction(ui->actionSearchWithoutLimit);
contextMnu->exec(QCursor::pos());
delete(contextMnu);
}
void ChatWidget::chatCharFormatChanged()
{
if (inChatCharFormatChanged) {
return;
}
inChatCharFormatChanged = true;
// Reset font and color before inserting a character if edit box is empty
// (color info disappears when the user deletes all text)
if (ui->chatTextEdit->toPlainText().isEmpty()) {
setColorAndFont(false);
}
inChatCharFormatChanged = false;
}
void ChatWidget::resetStatusBar()
{
ui->typingLabel->clear();
ui->typingPixmapLabel->clear();
typing = false;
emit infoChanged(this);
}
void ChatWidget::updateStatusTyping()
{
if(Settings->getChatDoNotSendIsTyping())
return;
if (time(NULL) - lastStatusSendTime > 5) // limit 'peer is typing' packets to at most every 10 sec
{
#ifdef ONLY_FOR_LINGUIST
tr("is typing...");
#endif
rsMsgs->sendStatusString(chatId, "is typing...");
lastStatusSendTime = time(NULL) ;
}
}
void ChatWidget::updateLenOfChatTextEdit()
{
if(sendingBlocked) return;
QTextEdit *chatWidget = ui->chatTextEdit;
QString text;
RsHtml::optimizeHtml(chatWidget, text);
std::wstring msg = text.toStdWString();
uint32_t maxMessageSize = this->maxMessageSize();
int charRemains = 0;
if (maxMessageSize > 0) {
charRemains = maxMessageSize - msg.length();
}
ui->sendButton->setEnabled(charRemains>=0);
if (charRemains>0)
text = tr("It remains %1 characters\nafter HTML conversion.").arg(charRemains);
else if(charRemains<0)
text = tr("Warning: This message is too big of %1 characters\nafter HTML conversion.").arg((0-charRemains));
else
text = "";
ui->sendButton->setToolTip(text);
ui->chatTextEdit->setToolTip(text);
}
void ChatWidget::sendChat()
{
if (!ui->sendButton->isEnabled()){
//Something block sending
return;
}
QTextEdit *chatWidget = ui->chatTextEdit;
if (chatWidget->toPlainText().isEmpty()) {
// nothing to send
return;
}
QString text;
if (ui->actionSendAsPlainText->isChecked()){
text = chatWidget->toPlainText();
text.replace(QChar(-4),"");//Char used when image on text.
} else {
RsHtml::optimizeHtml(chatWidget, text,
(ui->actionNoEmbed->isChecked() ? RSHTML_FORMATTEXT_NO_EMBED : 0)
+ (ui->actionSend_as_CommonMark->isChecked() ? RSHTML_FORMATTEXT_USE_CMARK : 0) );
}
std::string msg = text.toUtf8().constData();
if (msg.empty()) {
// nothing to send
return;
}
#ifdef CHAT_DEBUG
std::cout << "ChatWidget:sendChat " << std::endl;
#endif
rsMsgs->sendChat(chatId, msg);
chatWidget->clear();
// workaround for Qt bug - http://bugreports.qt.nokia.com/browse/QTBUG-2533
// QTextEdit::clear() does not reset the CharFormat if document contains hyperlinks that have been accessed.
chatWidget->setCurrentCharFormat(QTextCharFormat ());
}
void ChatWidget::on_closeInfoFrameButton_clicked()
{
ui->infoFrame->setVisible(false);
}
void ChatWidget::on_searchButton_clicked(bool bValue)
{
if (firstSearch)
qpSave_leSearch=ui->leSearch->palette();
removeFoundText();
ui->searchBefore->setVisible(false);//findText set it to true
ui->searchAfter->setVisible(false);//findText set it to true
ui->leSearch->setPalette(qpSave_leSearch);
if (bValue) {
ui->leSearch->setFocus();
if (!ui->leSearch->isVisible()){//Take text selected if leSearch is Invisible
if (ui->textBrowser->textCursor().selectedText().length()>0) {
ui->leSearch->setText(ui->textBrowser->textCursor().selectedText());
findText(ui->leSearch->text());
} else if(ui->chatTextEdit->textCursor().selectedText().length()>0) {
ui->leSearch->setText(ui->chatTextEdit->textCursor().selectedText());
findText(ui->leSearch->text());
}
}
if (!ui->leSearch->text().isEmpty())
findText(ui->leSearch->text());
} else {
//Erase last result Cursor
QTextDocument *qtdDocument = ui->textBrowser->document();
qtcCurrent=QTextCursor(qtdDocument);
}
ui->leSearch->setVisible(bValue);
ui->markButton->setVisible(bValue);
}
void ChatWidget::on_searchBefore_clicked()
{
findText(ui->leSearch->text(),true,true);
}
void ChatWidget::on_searchAfter_clicked()
{
findText(ui->leSearch->text(),false,true);
}
void ChatWidget::toogle_FindCaseSensitively()
{
bFindCaseSensitively=!bFindCaseSensitively;
}
void ChatWidget::toogle_FindWholeWords()
{
bFindWholeWords=!bFindWholeWords;
}
void ChatWidget::toogle_MoveToCursor()
{
bMoveToCursor=!bMoveToCursor;
}
void ChatWidget::toogle_SeachWithoutLimit()
{
bSearchWithoutLimit=!bSearchWithoutLimit;
}
bool ChatWidget::findText(const QString& qsStringToFind)
{
return findText(qsStringToFind, false,false);
}
bool ChatWidget::findText(const QString& qsStringToFind, bool bBackWard, bool bForceMove)
{
QTextDocument *qtdDocument = ui->textBrowser->document();
bool bFound = false;
bool bFirstFound = true;
uint uiFoundCount = 0;
removeFoundText();
if (qsLastsearchText!=qsStringToFind)
qtcCurrent=QTextCursor(qtdDocument);
qsLastsearchText=qsStringToFind;
if (!qsStringToFind.isEmpty())
{
QPalette qpBackGround=ui->leSearch->palette();
QTextCursor qtcHighLight(qtdDocument);
QTextCursor qtcCursor(qtdDocument);
QTextCharFormat qtcfPlainFormat(qtcHighLight.charFormat());
QTextCharFormat qtcfColorFormat = qtcfPlainFormat;
qtcfColorFormat.setBackground(QBrush(cFoundColor));
if (ui->textBrowser->textCursor().selectedText().length()>0)
qtcCurrent=ui->textBrowser->textCursor();
if (bBackWard) qtcHighLight.setPosition(qtdDocument->characterCount()-1);
qtcCursor.beginEditBlock();
while(!qtcHighLight.isNull()
&& ( (!bBackWard && !qtcHighLight.atEnd())
|| (bBackWard && !qtcHighLight.atStart())
))
{
QTextDocument::FindFlags qtdFindFlag;
if (bFindCaseSensitively) qtdFindFlag|=QTextDocument::FindCaseSensitively;
if (bFindWholeWords) qtdFindFlag|=QTextDocument::FindWholeWords;
if (bBackWard) qtdFindFlag|=QTextDocument::FindBackward;
qtcHighLight=qtdDocument->find(qsStringToFind,qtcHighLight, qtdFindFlag);
if(!qtcHighLight.isNull())
{
bFound=true;
if (!bFirstFound)
{
if (smFoundCursor.size()<uiMaxSearchLimitColor || bSearchWithoutLimit)// stop after uiMaxSearchLimitColor
{
QTextCharFormat qtcfSave= qtcHighLight.charFormat();
smFoundCursor[qtcHighLight]=qtcfSave;
qtcHighLight.mergeCharFormat(qtcfColorFormat);
}
}
if (bFirstFound &&
((bBackWard && (qtcHighLight.position()<qtcCurrent.position()))
|| (!bBackWard && (qtcHighLight.position()>qtcCurrent.position()))
))
{
bFirstFound=false;
qtcCurrent=qtcHighLight;
if (bMoveToCursor || bForceMove) ui->textBrowser->setTextCursor(qtcHighLight);
}
if (uiFoundCount<UINT_MAX)
uiFoundCount+=1;
}
}
if (bFound)
{
qpBackGround.setColor(QPalette::Base,QColor(0,200,0));
ui->leSearch->setToolTip(QString::number(uiFoundCount)+tr(" items found."));
} else {
qpBackGround.setColor(QPalette::Base,QColor(200,0,0));
ui->leSearch->setToolTip(tr("No items found."));
}
ui->leSearch->setPalette(qpBackGround);
qtcCursor.endEditBlock();
ui->searchBefore->setVisible((!bFirstFound || (!bBackWard && bFound)));
ui->searchAfter->setVisible((!bFirstFound || (bBackWard && bFound)));
firstSearch = false;
} else { //if (!qsStringToFind.isEmpty())
ui->leSearch->setPalette(qpSave_leSearch);
}
return bFound;
}
void ChatWidget::removeFoundText()
{
for(std::map<QTextCursor,QTextCharFormat>::const_iterator it=smFoundCursor.begin();it!=smFoundCursor.end();++it)
{
QTextCursor qtcCurrent=it->first;
QTextCharFormat qtcfCurrent=it->second;
qtcCurrent.setCharFormat(qtcfCurrent);
}
smFoundCursor.clear();
}
void ChatWidget::on_markButton_clicked(bool bValue)
{
if (bValue)
{
if (ui->textBrowser->textCursor().selectedText().length()>0)
{
qtcMark=ui->textBrowser->textCursor();
ui->markButton->setToolTip(tr("<b>Return to marked text</b><br><i>Ctrl+M</i>"));
} else { bValue=false;}
} else {
if (qtcMark.position()!=0)
{
ui->textBrowser->setTextCursor(qtcMark);
qtcMark=QTextCursor(ui->textBrowser->document());
ui->markButton->setToolTip(tr("<b>Mark this selected text</b><br><i>Ctrl+M</i>"));
}
}
ui->markButton->setChecked(bValue);
}
void ChatWidget::chooseColor()
{
bool ok;
QRgb color = QColorDialog::getRgba(ui->chatTextEdit->textColor().rgba(), &ok, window());
if (ok) {
currentColor = QColor(color);
PeerSettings->setPrivateChatColor(chatId, currentColor.name());
colorChanged();
setColorAndFont(false);
}
}
void ChatWidget::colorChanged()
{
QPixmap pix(16, 16);
pix.fill(currentColor);
ui->actionChooseColor->setIcon(pix);
}
void ChatWidget::chooseFont()
{
bool ok;
//Use NULL as parent as with this QFontDialog don't take care of title nether options.
QFont font = misc::getFont(&ok, currentFont, nullptr, tr("Choose your font."));
if (ok) {
currentFont = font;
setFont();
}
}
void ChatWidget::resetFont()
{
currentFont.fromString(Settings->getChatScreenFont());
setFont();
}
void ChatWidget::resetFonts()
{
currentFont.fromString(Settings->getChatScreenFont());
setColorAndFont(true);
PeerSettings->setPrivateChatFont(chatId, currentFont.toString());
}
void ChatWidget::setColorAndFont(bool both)
{
ui->chatTextEdit->setFont(currentFont);
QStringList fontdata=currentFont.toString().split(",");
QString stylesheet="font-family: '"+fontdata[0]+"'; font-size: "+fontdata[1]+"pt;";
if (currentFont.bold()) stylesheet+=" font-weight: bold;";
if (currentFont.italic()) stylesheet+=" font-style: italic;";
if (currentFont.underline()) stylesheet+=" text-decoration: underline;";
if (both) ui->textBrowser->setStyleSheet(stylesheet);
ui->chatTextEdit->setStyleSheet(stylesheet);
ui->chatTextEdit->setTextColor(currentColor);
ui->chatTextEdit->setFocus();
}
void ChatWidget::setFont()
{
setColorAndFont(false);
PeerSettings->setPrivateChatFont(chatId, currentFont.toString());
}
void ChatWidget::smileyWidget()
{
Emoticons::showSmileyWidget(this, ui->emoteiconButton, SLOT(addSmiley()), true);
}
void ChatWidget::addSmiley()
{
QString smiley = qobject_cast<QPushButton*>(sender())->toolTip().split("|").first();
// add trailing space
smiley += QString(" ");
// add preceding space when needed (not at start of text or preceding space already exists)
if(!ui->chatTextEdit->textCursor().atStart() && ui->chatTextEdit->toPlainText()[ui->chatTextEdit->textCursor().position() - 1] != QChar(' '))
smiley = QString(" ") + smiley;
ui->chatTextEdit->textCursor().insertText(smiley);
}
void ChatWidget::stickerWidget()
{
Emoticons::showStickerWidget(this, ui->stickerButton, SLOT(sendSticker()), true);
}
void ChatWidget::sendSticker()
{
if(sendingBlocked) return;
QString sticker = qobject_cast<QPushButton*>(sender())->statusTip();
QString encodedImage;
if (RsHtml::makeEmbeddedImage(sticker, encodedImage, 640*480, maxMessageSize() - 200)) { //-200 for the html stuff
RsHtml::optimizeHtml(encodedImage, 0);
std::string msg = encodedImage.toUtf8().constData();
rsMsgs->sendChat(chatId, msg);
}
}
void ChatWidget::clearChatHistory()
{
ui->textBrowser->clear();
on_searchButton_clicked(false);
ui->markButton->setChecked(false);
if (chatType() == CHATTYPE_LOBBY) {
if (notify) notify->chatLobbyCleared(chatId.toLobbyId(),"");
}
rsMsgs->clearChatLobby(chatId);
}
void ChatWidget::deleteChatHistory()
{
if ((QMessageBox::question(this, "RetroShare", tr("Do you really want to physically delete the history?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)) == QMessageBox::Yes) {
clearChatHistory();
rsHistory->clear(chatId);
}
}
void ChatWidget::messageHistory()
{
if (!imBrowser)
imBrowser = new ImHistoryBrowser(chatId, ui->chatTextEdit, this->title, window());
imBrowser->show();
}
void ChatWidget::addExtraFile()
{
QStringList files;
if (misc::getOpenFileNames(this, RshareSettings::LASTDIR_EXTRAFILE, tr("Add Extra File"), "", files)) {
ui->hashBox->addAttachments(files,mDefaultExtraFileFlags /*, 0*/);
}
}
void ChatWidget::addExtraPicture()
{
// select a picture file
QString file;
if (misc::getOpenFileName(window(), RshareSettings::LASTDIR_IMAGES, tr("Load Picture File"), "Pictures (*.png *.xpm *.jpg *.jpeg *.gif *.webp )", file)) {
QString encodedImage;
uint32_t maxMessageSize = this->maxMessageSize();
if (RsHtml::makeEmbeddedImage(file, encodedImage, 640*480, maxMessageSize - 200)) { //-200 for the html stuff
QTextDocumentFragment fragment = QTextDocumentFragment::fromHtml(encodedImage);
ui->chatTextEdit->textCursor().insertFragment(fragment);
}
}
}
void ChatWidget::fileHashingFinished(QList<HashedFile> hashedFiles)
{
std::cerr << "ChatWidget::fileHashingFinished() started." << std::endl;
QString message;
QList<HashedFile>::iterator it;
for (it = hashedFiles.begin(); it != hashedFiles.end(); ++it) {
HashedFile& hashedFile = *it;
QString ext = QFileInfo(hashedFile.filename).suffix().toUpper();
RetroShareLink link;
// We dont use extra links anymore, since files in the extra list can always be accessed using anonymous+encrypted FT.
link = RetroShareLink::createFile(hashedFile.filename, hashedFile.size, QString::fromStdString(hashedFile.hash.toStdString()));
if (hashedFile.flag & HashedFile::Picture) {
message += QString("<img src=\"file:///%1\" width=\"100\" height=\"100\">").arg(hashedFile.filepath);
message+="<br>";
} else {
bool preview = false;
if(hashedFiles.size()==1 && (ext == "JPG" || ext == "PNG" || ext == "JPEG" || ext == "GIF"))
{
QString encodedImage;
uint32_t maxMessageSize = this->maxMessageSize();
if (RsHtml::makeEmbeddedImage(hashedFile.filepath, encodedImage, 640*480, maxMessageSize - 200 - link.toHtmlSize().length()))
{ QTextDocumentFragment fragment = QTextDocumentFragment::fromHtml(encodedImage);
ui->chatTextEdit->textCursor().insertFragment(fragment);
preview=true;
}
}
if(!preview)
{
QString image = FilesDefs::getImageFromFilename(hashedFile.filename, false);
if (!image.isEmpty()) {
message += QString("<img src=\"%1\">").arg(image);
}
}
}
message += link.toHtmlSize();
if (it != hashedFiles.end()) {
message += "<BR>";
}
}
#ifdef CHAT_DEBUG
std::cerr << "ChatWidget::fileHashingFinished message : " << message.toStdString() << std::endl;
#endif
ui->chatTextEdit->insertHtml(message);
}
bool ChatWidget::fileSave()
{
if (fileName.isEmpty())
return fileSaveAs();
QFile file(fileName);
if (!file.open(QFile::WriteOnly))
return false;
QTextStream ts(&file);
ts.setCodec(QTextCodec::codecForName("UTF-8"));
ts << ui->textBrowser->document()->toPlainText();
ui->textBrowser->document()->setModified(false);
return true;
}
bool ChatWidget::fileSaveAs()
{
QString fn;
if (misc::getSaveFileName(window(), RshareSettings::LASTDIR_HISTORY, tr("Save as..."), tr("Text File (*.txt );;All Files (*)"), fn)) {
setCurrentFileName(fn);
return fileSave();
}
return false;
}
void ChatWidget::setCurrentFileName(const QString &fileName)
{
this->fileName = fileName;
ui->textBrowser->document()->setModified(false);
setWindowModified(false);
}
void ChatWidget::updateStatus(const QString &peer_id, int status)
{
if (! (chatType() == CHATTYPE_PRIVATE || chatType() == CHATTYPE_DISTANT))
{
// updateTitle is used
return;
}
// make virtual peer id from gxs id in case of distant chat
RsPeerId vpid;
if(chatId.isDistantChatId())
vpid = RsPeerId(chatId.toDistantChatId());
else
vpid = chatId.toPeerId();
/* set font size for status */
if (peer_id.toStdString() == vpid.toStdString())
{
// the peers status has changed
QString tooltip_info ;
QString peerName ;
if(chatId.isDistantChatId())
{
DistantChatPeerInfo dcpinfo ;
RsIdentityDetails details ;
if(rsMsgs->getDistantChatStatus(chatId.toDistantChatId(),dcpinfo))
{
if(rsIdentity->getIdDetails(dcpinfo.to_id,details))
peerName = QString::fromUtf8( details.mNickname.c_str() ) ;
else
peerName = QString::fromStdString(dcpinfo.to_id.toStdString()) ;
tooltip_info = QString("Identity Id: ")+QString::fromStdString(dcpinfo.to_id.toStdString());
}
else
{
peerName = QString::fromStdString(chatId.toDistantChatId().toStdString()) ;
tooltip_info = QString("Identity Id: unknown (bug?)");
}
}
else
{
peerName = QString::fromUtf8(rsPeers->getPeerName(chatId.toPeerId()).c_str());
tooltip_info = QString("Peer Id: ") + QString::fromStdString(chatId.toPeerId().toStdString());
}
// is scrollbar at the end?
QScrollBar *scrollbar = ui->textBrowser->verticalScrollBar();
bool atEnd = (scrollbar->value() == scrollbar->maximum());
switch (status) {
case RS_STATUS_OFFLINE:
ui->infoFrame->setVisible(true);
ui->infoLabel->setText(peerName + " " + tr("appears to be Offline.") +"\n" + tr("Messages you send will be delivered after Friend is again Online."));
break;
case RS_STATUS_INACTIVE:
ui->infoFrame->setVisible(true);
ui->infoLabel->setText(peerName + " " + tr("is Idle and may not reply"));
break;
case RS_STATUS_ONLINE:
ui->infoFrame->setVisible(false);
break;
case RS_STATUS_AWAY:
ui->infoLabel->setText(peerName + " " + tr("is Away and may not reply"));
ui->infoFrame->setVisible(true);
break;
case RS_STATUS_BUSY:
ui->infoLabel->setText(peerName + " " + tr("is Busy and may not reply"));
ui->infoFrame->setVisible(true);
break;
}
ui->titleLabel->setText(peerName);
ui->titleLabel->setToolTip(tooltip_info);
ui->statusLabel->setText(QString("(%1)").arg(StatusDefs::name(status)));
peerStatus = status;
if (atEnd) {
// scroll to the end
scrollbar->setValue(scrollbar->maximum());
}
emit infoChanged(this);
emit statusChanged(status);
// Notify all ChatWidgetHolder
foreach (ChatWidgetHolder *chatWidgetHolder, mChatWidgetHolder) {
chatWidgetHolder->updateStatus(status);
}
return;
}
// ignore status change
}
void ChatWidget::updateTitle()
{
if (chatType() != CHATTYPE_LOBBY) {
// updateStatus is used
return;
}
ui->titleLabel->setText(RsHtml::plainText(name) + "@" + RsHtml::plainText(title));
}
void ChatWidget::updatePeersCustomStateString(const QString& /*peer_id*/, const QString& /*status_string*/)
{
QString status_text;
// TODO: fix peer_id and types and eveyrhing
/*
if (RsPeerId(peer_id.toStdString()) == peerId) {
// the peers status string has changed
if (status_string.isEmpty()) {
ui->statusMessageLabel->hide();
ui->titleLabel->setAlignment ( Qt::AlignTop );
ui->statusLabel->setAlignment ( Qt::AlignTop );
} else {
ui->statusMessageLabel->show();
status_text = RsHtml().formatText(NULL, status_string, RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS);
ui->statusMessageLabel->setText(status_text);
ui->titleLabel->setAlignment ( Qt::AlignVCenter );
ui->statusLabel->setAlignment ( Qt::AlignVCenter );
}
}
*/
}
void ChatWidget::updateStatusString(const QString &statusMask, const QString &statusString, bool permanent)
{
ui->typingLabel->setText(QString(statusMask).arg(trUtf8(statusString.toUtf8()))); // displays info for 5 secs.
ui->typingPixmapLabel->setPixmap(FilesDefs::getPixmapFromQtResourcePath(":icons/png/typing.png") );
if (statusString == "is typing...") {
typing = true;
emit infoChanged(this);
}
if(!permanent)
QTimer::singleShot(5000, this, SLOT(resetStatusBar())) ;
}
void ChatWidget::setName(const QString &name)
{
this->name = name;
updateTitle();
}
bool ChatWidget::setStyle()
{
if (style.showDialog(window())) {
PeerSettings->setStyle(chatId, "PopupChatDialog", style);
return true;
}
return false;
}
void ChatWidget::setUseCMark(const bool bUseCMark)
{
useCMark = bUseCMark;
ui->cmPreview->setVisible(useCMark);
updateCMPreview();
}
void ChatWidget::updateCMPreview()
{
if (!useCMark) return;
QString message = ui->chatTextEdit->toHtml();
QString formattedMessage = RsHtml().formatText(ui->cmPreview->document(), message, RSHTML_FORMATTEXT_USE_CMARK);
ui->cmPreview->setHtml(formattedMessage);
}
void ChatWidget::quote()
{
QString text = RsHtml::makeQuotedText(ui->textBrowser);
emit ui->chatTextEdit->append(text);
}
void ChatWidget::dropPlacemark()
{
ui->textBrowser->moveCursor(QTextCursor::End); // *append* inserts text at end but with formatting in effect at
ui->textBrowser->append("----------"); // current cursor position, such as in the middle of a hotlink,
// which would be strange. This OTOH inserts text with
// formatting in effect on the last line, which may be strange
// or not.
}
void ChatWidget::saveImage()
{
QPoint point = ui->actionSave_image->data().toPoint();
QTextCursor cursor = ui->textBrowser->cursorForPosition(point);
ImageUtil::extractImage(window(), cursor);
}
void ChatWidget::saveSticker()
{
QPoint point = ui->actionImport_sticker->data().toPoint();
QTextCursor cursor = ui->textBrowser->cursorForPosition(point);
QString filename = QInputDialog::getText(window(), "Import sticker", "Sticker name");
if(filename.isEmpty()) return;
filename = Emoticons::importedStickerPath() + "/" + filename + ".png";
ImageUtil::extractImage(window(), cursor, filename);
}