QTfrontend/ui/widget/chatwidget.cpp
author Wuzzy <Wuzzy2@mail.ru>
Fri, 10 May 2019 23:24:56 +0200
changeset 14910 6b591186ab10
parent 14876 0f39f8efbd2e
child 15259 ebb86ed1b785
permissions -rw-r--r--
Server /watch: Add back KICKED for old clients, remove REPLAY_START parameter

/*
 * Hedgewars, a free turn based strategy game
 * Copyright (c) 2007 Igor Ulyanov <iulyanov@gmail.com>
 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <QDesktopServices>
#include <QTextBrowser>
#include <QAction>
#include <QFile>
#include <QTextStream>
#include <QMenu>
#include <QCursor>
#include <QItemSelectionModel>
#include <QDateTime>
#include <QTime>
#include <QListView>
#include <QModelIndexList>
#include <QSortFilterProxyModel>
#include <QMenu>
#include <QScrollBar>
#include <QMimeData>

#include "DataManager.h"
#include "hwconsts.h"
#include "gameuiconfig.h"
#include "playerslistmodel.h"
#include "HWApplication.h"
#include "chatwidget.h"


QString * HWChatWidget::s_styleSheet = NULL;
QStringList * HWChatWidget::s_displayNone = NULL;
bool HWChatWidget::s_isTimeStamped = true;
QString HWChatWidget::s_tsFormat = ":mm:ss";

const QString & HWChatWidget::styleSheet()
{
    if (s_styleSheet != NULL)
        return *s_styleSheet;

    setStyleSheet();

    return *s_styleSheet;
}

void HWChatWidget::setStyleSheet(const QString & styleSheet)
{
    QString orgStyleSheet = styleSheet;
    QString style = QString(orgStyleSheet);

    // no stylesheet supplied, search for one or use default
    if (orgStyleSheet.isEmpty())
    {
        // load external stylesheet if there is any
        QFile extFile("physfs://css/chat.css");

        QFile resFile(":/res/css/chat.css");

        QFile & file = (extFile.exists()?extFile:resFile);

        if (file.open(QIODevice::ReadOnly | QIODevice::Text))
        {
            QTextStream in(&file);
            while (!in.atEnd())
            {
                style.append(in.readLine()+"\n");
            }
            orgStyleSheet = style;

            file.close();
        }
    }

    // let's parse display:none; ...

    // prepare for MAGIC :D

    // matches (multi-)whitespaces (for replacement with simple space)
    QRegExp ws("\\s+");

    // matches comments (for removal)
    QRegExp rem("/\\*([^*]|\\*(?!/))*\\*/");

    // strip comments and multi-whitespaces to compress the style-sheet a bit
    style = style.remove(rem).replace(ws," ");


    // now let's see what messages the user does not want to be displayed
    // by checking for display:none; (since QTextBrowser does not support it)

    // MOAR MAGIC :DDD

    // matches definitions lacking display:none; (for removal)
    QRegExp displayed(
        "([^{}]*\\{)(?!([^}]*;)* ?display ?: ?none ?(;[^}]*)?\\})[^}]*\\}");

    // matches all {...} and , (used as seperator for splitting into names)
    QRegExp split(" *(\\{[^}]*\\}|,) *");

    // matches class names that are referenced without hierachy
    QRegExp nohierarchy("^.[^ .]+$");

    QStringList victims = QString(style).
                          remove(displayed). // remove visible stuff
                          trimmed().
                          split(split). // get a list of the names
                          filter(nohierarchy). // only direct class names
                          replaceInStrings(QRegExp("^."),""); // crop .


    if (victims.contains("timestamp"))
    {
        s_isTimeStamped = false;
        victims.removeAll("timestamp");
    }
    else
    {
        s_isTimeStamped = true;
        s_tsFormat =
            ((victims.contains("timestamp:hours"))?"":"hh:") +
            QString("mm") +
            ((victims.contains("timestamp:seconds"))?"":":ss");
    }

    victims.removeAll("timestamp:hours");
    victims.removeAll("timestamp:seconds");

    victims.removeDuplicates();

    QStringList * oldDisplayNone = s_displayNone;
    QString * oldStyleSheet = s_styleSheet;

    s_displayNone = new QStringList(victims);
    s_styleSheet = new QString(orgStyleSheet);

    if (oldDisplayNone != NULL)
        delete oldDisplayNone;

    if (oldStyleSheet != NULL)
        delete oldStyleSheet;

}

void HWChatWidget::displayError(const QString & message)
{
    addLine("msg_Error", " !!! " + message);
}


void HWChatWidget::displayNotice(const QString & message)
{
    addLine("msg_Notice", " *** " + message);
}


void HWChatWidget::displayWarning(const QString & message)
{
    addLine("msg_Warning", " *!* " + message);
}


HWChatWidget::HWChatWidget(QWidget* parent, bool notify) :
    QWidget(parent),
    mainLayout(this)
{
    this->gameSettings = NULL;
    this->notify = notify;

    m_usersModel = NULL;

    m_isAdmin = false;
    m_autoKickEnabled = false;

    m_scrollToBottom = false;
    m_scrollBarPos = 0;

    QStringList vpList =
         QStringList() << "Classic" << "Default" << "Mobster" << "Russian";

    foreach (const QString & vp, vpList)
    {
        m_helloSounds.append(QString("/Sounds/voices/%1/Hello.ogg").arg(vp));
    }

    m_hilightSound = "/Sounds/beep.ogg";

    mainLayout.setMargin(0);

    QWidget * leftSideContainer = new QWidget();
    leftSideContainer->setObjectName("leftSideContainer");
    leftSideContainer->setStyleSheet("#leftSideContainer { border-width: 0px; background-color: #ffcc00; border-radius: 10px;} QTextBrowser, SmartLineEdit { background-color: rgb(13, 5, 68); }");
    QVBoxLayout * leftSide = new QVBoxLayout(leftSideContainer);
    leftSide->setSpacing(3);
    leftSide->setMargin(3);
    mainLayout.addWidget(leftSideContainer, 76);

    // Chat view

    chatText = new QTextBrowser(this);
    chatText->setWhatsThis(tr("Chat log"));
    chatText->document()->setDefaultStyleSheet(styleSheet());
    chatText->setMinimumHeight(20);
    chatText->setMinimumWidth(10);
    chatText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    chatText->setOpenLinks(false);
    chatText->setStyleSheet("QTextBrowser { background-color: rgb(23, 11, 54); border-width: 0px; }");
    connect(chatText, SIGNAL(anchorClicked(const QUrl&)),
            this, SLOT(linkClicked(const QUrl&)));
    leftSide->addWidget(chatText, 1);

    // Input box

    // Normal:  rgb(23, 11, 54)
    // Hover:   rgb(13, 5, 68)

    chatEditLine = new SmartLineEdit();
    chatEditLine->setWhatsThis(tr("Enter chat messages here and send them with [Enter]"));
    chatEditLine->setMaxLength(300);
    chatEditLine->setStyleSheet("SmartLineEdit { background-color: rgb(23, 11, 54); padding: 2px 8px; border-width: 0px; border-radius: 7px; } SmartLineEdit:hover, SmartLineEdit:focus { background-color: rgb(13, 5, 68); }");
    chatEditLine->setFixedHeight(24);
    chatEditLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    connect(chatEditLine, SIGNAL(returnPressed()), this, SLOT(returnPressed()));

    leftSide->addWidget(chatEditLine, 0);

    // Nickname list

    chatNicks = new QListView(this);
    chatNicks->setWhatsThis(tr("List of players"));
    chatNicks->setIconSize(QSize(24, 16));
    chatNicks->setSelectionMode(QAbstractItemView::SingleSelection);
    chatNicks->setEditTriggers(QAbstractItemView::NoEditTriggers);
    chatNicks->setMinimumHeight(10);
    chatNicks->setMinimumWidth(10);
    chatNicks->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    chatNicks->setContextMenuPolicy(Qt::CustomContextMenu);

    connect(chatNicks, SIGNAL(doubleClicked(QModelIndex)),
            this, SLOT(chatNickDoubleClicked(QModelIndex)));

    connect(chatNicks, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(nicksContextMenuRequested(QPoint)));

    mainLayout.addSpacing(0);
    mainLayout.addWidget(chatNicks, 24);

    // the userData is used to flag things that are even available when user
    // is offline
    acInfo = new QAction(QAction::tr("Info"), chatNicks);
    acInfo->setIcon(QIcon(":/res/info.png"));
    acInfo->setData(QVariant(false));
    connect(acInfo, SIGNAL(triggered(bool)), this, SLOT(onInfo()));
    acKick = new QAction(QAction::tr("Kick"), chatNicks);
    acKick->setIcon(QIcon(":/res/kick.png"));
    acKick->setData(QVariant(false));
    connect(acKick, SIGNAL(triggered(bool)), this, SLOT(onKick()));
    acBan = new QAction(QAction::tr("Ban"), chatNicks);
    acBan->setIcon(QIcon(":/res/ban.png"));
    acBan->setData(QVariant(true));
    connect(acBan, SIGNAL(triggered(bool)), this, SLOT(onBan()));
    acDelegate = new QAction(QAction::tr("Delegate room control"), chatNicks);
    acDelegate->setIcon(QIcon(":/res/chat/roomadmin.png"));
    acDelegate->setData(QVariant(true));
    connect(acDelegate, SIGNAL(triggered(bool)), this, SLOT(onDelegate()));
    acFollow = new QAction(QAction::tr("Follow"), chatNicks);
    acFollow->setIcon(QIcon(":/res/follow.png"));
    acFollow->setData(QVariant(false));
    connect(acFollow, SIGNAL(triggered(bool)), this, SLOT(onFollow()));
    acIgnore = new QAction(QAction::tr("Ignore"), chatNicks);
    acIgnore->setIcon(QIcon(":/res/ignore.png"));
    acIgnore->setData(QVariant(true));
    connect(acIgnore, SIGNAL(triggered(bool)), this, SLOT(onIgnore()));
    acFriend = new QAction(QAction::tr("Add friend"), chatNicks);
    acFriend->setIcon(QIcon(":/res/addfriend.png"));
    acFriend->setData(QVariant(true));
    connect(acFriend, SIGNAL(triggered(bool)), this, SLOT(onFriend()));

    chatNicks->insertAction(0, acFriend);
    chatNicks->insertAction(0, acInfo);
    chatNicks->insertAction(0, acIgnore);

    setShowFollow(true);

    setAcceptDrops(true);

    m_nicksMenu = new QMenu(this);

    clear();
}

void HWChatWidget::setSettings(QSettings * settings)
{
    gameSettings = settings;
}

void HWChatWidget::linkClicked(const QUrl & link)
{
    if ((link.scheme() == "http") || (link.scheme() == "https"))
        QDesktopServices::openUrl(link);
    else if (link.scheme() == "hwnick")
    {
        // decode nick
        QString nick = QString::fromUtf8(QByteArray::fromBase64(link.query(QUrl::FullyDecoded).toLatin1()));
        QModelIndexList mil = chatNicks->model()->match(chatNicks->model()->index(0, 0), Qt::DisplayRole, nick);

        bool isOffline = (mil.size() < 1);

        if (isOffline)
        {
            m_clickedNick = nick;
            chatNicks->selectionModel()->clearSelection();
        }
        else
        {
            chatNicks->selectionModel()->select(mil[0], QItemSelectionModel::ClearAndSelect);
        }

        nicksContextMenuRequested(chatNicks->mapFromGlobal(QCursor::pos()));
    }
}

void HWChatWidget::setShowFollow(bool enabled)
{
    if (enabled)
    {
        if (!(chatNicks->actions().contains(acFollow)))
            chatNicks->insertAction(acFriend, acFollow);
    }
    else
    {
        if (chatNicks->actions().contains(acFollow))
            chatNicks->removeAction(acFollow);
    }
}

void HWChatWidget::setIgnoreListKick(bool enabled)
{
    m_autoKickEnabled = enabled;
}


void HWChatWidget::returnPressed()
{
    QStringList lines = chatEditLine->text().split('\n');
    chatEditLine->rememberCurrentText();
    foreach (const QString &line, lines)
    {
        // skip empty/whitespace lines
        if (line.trimmed().isEmpty())
            continue;

        if (!parseCommand(line))
            emit chatLine(line);
    }
    chatEditLine->clear();
}

// "link" nick, but before that encode it in base64 to make sure it can't
// intefere with html/url syntax the nick is put as querystring as putting
// it as host would convert it to it's lower case variant
QString HWChatWidget::linkedNick(const QString & nickname)
{
    // '[' and '(' are reserved characters used for fake player names in special server messages
    if ((nickname != m_userNick) && (!nickname.startsWith('[')) && (!nickname.startsWith('(')))
        // linked nick
        return QString("<a href=\"hwnick://?%1\" class=\"nick\">%2</a>").arg(
                   QString(nickname.toUtf8().toBase64())).arg(nickname.toHtmlEscaped());

    // unlinked nick (if own one or fake player name)
    return QString("<span class=\"nick\">%1</span>").arg(nickname.toHtmlEscaped());
}

// Regex to make some URLs clickable for selected domains:
// - hedgewars.org (official website)
// - hh.unit22.org (community addon server)
const QRegExp HWChatWidget::URLREGEXP = QRegExp("(http(s)?://)?(www\\.)?((([^/:?&#]+\\.)?hedgewars\\.org|hh\\.unit22\\.org)(/[^ ]*)?)");

bool HWChatWidget::containsHighlight(const QString & sender, const QString & message)
{
    if ((sender != m_userNick) && (!m_userNick.isEmpty()))
    {
        QString lcStr = message.toLower();

        foreach (const QRegExp & hl, m_highlights)
        {
            if (lcStr.contains(hl))
                return true;
        }
    }
    return false;
}

QString HWChatWidget::messageToHTML(const QString & message)
{
    QString formattedStr = message.toHtmlEscaped();
    // link some URLs
    formattedStr = formattedStr.replace(URLREGEXP, "<a href=\"http\\2://\\4\">\\4</a>");
    return formattedStr;
}

void HWChatWidget::onChatAction(const QString & nick, const QString & action)
{
    printChatString(nick, "* " + linkedNick(nick) + " " + messageToHTML(action), "Action", containsHighlight(nick, action));
}

void HWChatWidget::onChatMessage(const QString & nick, const QString & message)
{
    printChatString(nick, linkedNick(nick) + ": " + messageToHTML(message), "Chat", containsHighlight(nick, message));
}

void HWChatWidget::printChatString(
    const QString & nick, const QString & str, const QString & cssClassPart, bool highlight)
{
    if(!m_usersModel)
        return;

    // don't show chat lines that are from ignored nicks
    if (m_usersModel->isFlagSet(nick, PlayersListModel::Ignore))
        return;

    bool isFriend = (!nick.isEmpty()) && m_usersModel->isFlagSet(nick, PlayersListModel::Friend);

    QString cssClass = (isFriend ? "msg_Friend" : "msg_User") + cssClassPart;

    addLine(cssClass, str, highlight);
}

bool HWChatWidget::isInGame() {
    if (!m_usersModel)
        return false;

    return m_usersModel->isFlagSet(m_userNick, PlayersListModel::InGame);
}

void HWChatWidget::addLine(const QString & cssClass, QString line, bool isHighlight)
{
    if (s_displayNone->contains(cssClass))
        return; // the css forbids us to display this line

    beforeContentAdd();

    if (chatStrings.size() > 250)
        chatStrings.removeFirst();

    if (s_isTimeStamped)
    {
        QString tsMarkUp = "<span class=\"timestamp\">[%1]</span> ";
        QTime now = QDateTime::currentDateTime().time();
        line = tsMarkUp.arg(now.toString(s_tsFormat)) + line;
    }

    line = QString("<span class=\"%1\">%2</span>").arg(cssClass).arg(line);

    if (isHighlight)
    {
        line = QString("<span class=\"highlight\">%1</span>").arg(line);
        SDLInteraction::instance().playSoundFile(m_hilightSound);
        if (!isInGame())
            HWApplication::alert(this, 800);
    }

    chatStrings.append(line);

    chatText->setHtml("<html><body>"+chatStrings.join("<br>")+"</body></html>");

    afterContentAdd();
}

void HWChatWidget::onServerMessage(const QString& str)
{
    beforeContentAdd();

    if (chatStrings.size() > 250)
        chatStrings.removeFirst();

    chatStrings.append("<hr>" + str + "<hr>");

    chatText->setHtml("<html><body>"+chatStrings.join("<br>")+"</body></html>");

    afterContentAdd();
}


void HWChatWidget::nickAdded(const QString & nick, bool notifyNick)
{
    QSortFilterProxyModel * playersSortFilterModel = qobject_cast<QSortFilterProxyModel *>(chatNicks->model());
    if(!playersSortFilterModel)
        return;

    PlayersListModel * players = qobject_cast<PlayersListModel *>(playersSortFilterModel->sourceModel());

    if(!players)
        return;

    bool isIgnored = players->isFlagSet(nick, PlayersListModel::Ignore);

    if (isIgnored && m_isAdmin && m_autoKickEnabled)
    {
        emit kick(nick);
        return;
    }

    if ((!isIgnored) && (nick != m_userNick)) // don't auto-complete own name
        chatEditLine->addNickname(nick);

    emit nickCountUpdate(chatNicks->model()->rowCount());

    if (!isIgnored)
        printChatString(nick, QString("*** ") + tr("%1 has joined").arg(linkedNick(nick)), "Join", false);

    if (notifyNick && notify)
    {
        if (m_helloSounds.size() > 0)
        {
            SDLInteraction::instance().playSoundFile(
                            m_helloSounds.at(rand() % m_helloSounds.size()));
        }        

        if (!isInGame())
        {
            HWApplication::alert(this, 2000);
        }
    }
}

void HWChatWidget::nickRemoved(const QString& nick)
{
    nickRemoved(nick, "");
}

void HWChatWidget::nickRemoved(const QString& nick, const QString & message)
{
    chatEditLine->removeNickname(nick);

    emit nickCountUpdate(chatNicks->model()->rowCount());

    // Normal quit
    if (message.isEmpty() || message == "bye")
    {
        printChatString(nick, QString("*** ") + tr("%1 has left").arg(linkedNick(nick)), "Leave", false);
    }
    // Quit with additional server message (i.e. ping timeout)
    else
    {
        printChatString(nick, QString("*** ") + tr("%1 has left (%2)").arg(linkedNick(nick)).arg(HWApplication::translate("server", message.toLatin1().constData()).toHtmlEscaped()), "Leave", false);
    }
}

void HWChatWidget::clear()
{
    chatEditLine->reset();

    // add default commands
    QStringList cmds;
    // /saveStyleSheet is(/was?) broken because of Physfs or something
    cmds << "/clear" << "/help" << "/info" << "/me" << "/quit" << "/rnd";
    chatEditLine->addCommands(cmds);

    chatText->clear();
    chatStrings.clear();
    //chatNicks->clear();

    // clear and re compile regexp for highlighting
    m_highlights.clear();

    QString hlRegExp("^(.* )?%1[^-a-z0-9_]*( .*)?$");
    QRegExp whitespace("\\s");

    if (!m_userNick.isEmpty())
        m_highlights.append(QRegExp(hlRegExp.arg(QRegExp::escape(m_userNick.toLower()))));

    QFile file(cfgdir->absolutePath() + "/" + m_userNick.toLower() + "_highlight.txt");

    if (file.exists() && (file.open(QIODevice::ReadOnly | QIODevice::Text)))
    {
        QTextStream in(&file);
        while (!in.atEnd())
        {
            QString line = in.readLine();
            QStringList list = line.split(whitespace);
            foreach (QString word, list)
            {
                m_highlights.append(QRegExp(
                                        hlRegExp.arg(QRegExp::escape(word.toLower()))));
            }
        }

        if (file.isOpen())
            file.close();
    }

    QFile file2(cfgdir->absolutePath() + "/" + m_userNick.toLower() + "_hlregexp.txt");

    if (file2.exists() && (file2.open(QIODevice::ReadOnly | QIODevice::Text)))
    {
        QTextStream in(&file2);
        while (!in.atEnd())
        {
            m_highlights.append(QRegExp(in.readLine().toLower()));
        }

        if (file2.isOpen())
            file2.close();
    }
}

void HWChatWidget::onPlayerInfo(
            const QString & nick,
            const QString & ip,
            const QString & version,
            const QString & roomInfo)
{
    addLine("msg_PlayerInfo", QString(" >>> %1 - <span class=\"ipaddress\">%2</span> <span class=\"version\">%3</span> <span class=\"location\">%4</span>")
        .arg(linkedNick(nick))
        .arg(QString(ip == "[]"?"":ip).toHtmlEscaped())
        .arg(version.toHtmlEscaped())
        .arg(roomInfo.toHtmlEscaped())
    );
}

void HWChatWidget::onKick()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    if(mil.size())
        emit kick(mil[0].data().toString());
}

void HWChatWidget::onBan()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    if(mil.size())
        emit ban(mil[0].data().toString());
}

void HWChatWidget::onDelegate()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    if(mil.size())
        emit delegate(mil[0].data().toString());
}

void HWChatWidget::onInfo()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    if(mil.size())
        emit info(mil[0].data().toString());
}

void HWChatWidget::onFollow()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    if(mil.size())
        emit follow(mil[0].data().toString());
}

void HWChatWidget::onIgnore()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    QString nick;
    if(mil.size())
        nick = mil[0].data().toString();
    else
        nick = m_clickedNick;

    QSortFilterProxyModel * playersSortFilterModel = qobject_cast<QSortFilterProxyModel *>(chatNicks->model());
    if(!playersSortFilterModel)
        return;

    PlayersListModel * players = qobject_cast<PlayersListModel *>(playersSortFilterModel->sourceModel());

    if(!players)
        return;

    if(players->isFlagSet(nick, PlayersListModel::Ignore))
    {
        players->setFlag(nick, PlayersListModel::Ignore, false);
        chatEditLine->addNickname(nick);
        displayNotice(tr("%1 has been removed from your ignore list").arg(linkedNick(nick)));
    }
    else // not on list - add
    {
        // don't consider ignored people friends
        if(players->isFlagSet(nick, PlayersListModel::Friend))
            emit onFriend();

        players->setFlag(nick, PlayersListModel::Ignore, true);
        chatEditLine->removeNickname(nick);
        displayNotice(tr("%1 has been added to your ignore list").arg(linkedNick(nick)));
    }

    if(mil.size())
        chatNicks->scrollTo(chatNicks->selectionModel()->selectedRows()[0]);
}

void HWChatWidget::onFriend()
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    QString nick;
    if(mil.size())
        nick = mil[0].data().toString();
    else
        nick = m_clickedNick;

    QSortFilterProxyModel * playersSortFilterModel = qobject_cast<QSortFilterProxyModel *>(chatNicks->model());
    if(!playersSortFilterModel)
        return;

    PlayersListModel * players = qobject_cast<PlayersListModel *>(playersSortFilterModel->sourceModel());

    if(!players)
        return;

    if(players->isFlagSet(nick, PlayersListModel::Friend))
    {
        players->setFlag(nick, PlayersListModel::Friend, false);
        chatEditLine->removeNickname(nick);
        displayNotice(tr("%1 has been removed from your friends list").arg(linkedNick(nick)));
    }
    else // not on list - add
    {
        if(players->isFlagSet(nick, PlayersListModel::Ignore))
            emit onIgnore();

        players->setFlag(nick, PlayersListModel::Friend, true);
        chatEditLine->addNickname(nick);
        displayNotice(tr("%1 has been added to your friends list").arg(linkedNick(nick)));
    }

    if(mil.size())
        chatNicks->scrollTo(chatNicks->selectionModel()->selectedRows()[0]);
}

void HWChatWidget::chatNickDoubleClicked(const QModelIndex &index)
{
    m_clickedNick = index.data().toString();

    QList<QAction *> actions = chatNicks->actions();
    actions.first()->activate(QAction::Trigger);
}


void HWChatWidget::adminAccess(bool b)
{
    chatNicks->removeAction(acKick);
    //chatNicks->removeAction(acBan);
    chatNicks->removeAction(acDelegate);

    m_isAdmin = b;

    if(b)
    {
        chatNicks->insertAction(0, acKick);
        //chatNicks->insertAction(0, acBan);
        chatNicks->insertAction(acFriend, acDelegate);
    }
}

void HWChatWidget::dragEnterEvent(QDragEnterEvent * event)
{
    if (event->mimeData()->hasUrls())
    {
        QList<QUrl> urls = event->mimeData()->urls();
        if (urls.count() == 1)
        {
            QUrl url = urls[0];

            static QRegExp localFileRegExp("file://.*\\.css$");
            localFileRegExp.setCaseSensitivity(Qt::CaseInsensitive);

            if (url.toString().contains(localFileRegExp))
                event->acceptProposedAction();
        }
    }
}

void HWChatWidget::dropEvent(QDropEvent * event)
{
    const QString path(event->mimeData()->urls()[0].toString());

    QFile file(event->mimeData()->urls()[0].toLocalFile());

    if (file.exists() && (file.open(QIODevice::ReadOnly | QIODevice::Text)))
    {
        QString style;
        QTextStream in(&file);
        while (!in.atEnd())
        {
            QString line = in.readLine();
            style.append(line + "\n");
        }

        setStyleSheet(style);
        chatText->document()->setDefaultStyleSheet(*s_styleSheet);
        displayNotice(tr("Stylesheet imported from %1").arg(path));
        displayNotice(tr("Enter %1 if you want to use the current StyleSheet in future, enter %2 to reset!").arg("/saveStyleSheet").arg("/discardStyleSheet"));

        if (file.isOpen())
            file.close();

        event->acceptProposedAction();
    }
    else
        displayError(tr("Couldn't read %1").arg(event->mimeData()->urls()[0].toString()));
}


void HWChatWidget::discardStyleSheet()
{
    setStyleSheet();
    chatText->document()->setDefaultStyleSheet(*s_styleSheet);
    displayNotice(tr("StyleSheet discarded"));
}


void HWChatWidget::saveStyleSheet()
{
    QString dest = "physfs://css/chat.css";

    QFile file(dest);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream out(&file);
        QStringList lines = s_styleSheet->split("\n", QString::KeepEmptyParts);

        // strip trailing empty lines
        while (lines.last().isEmpty())
            lines.takeLast();

        foreach (const QString & line, lines)
        {
            out << line << endl;
        }
        out << endl;
        file.close();
        displayNotice(tr("StyleSheet saved to %1").arg(dest));
    }
    else
        displayError(tr("Failed to save StyleSheet to %1").arg(dest));
}


bool HWChatWidget::parseCommand(const QString & line)
{
    if (line[0] == '/')
    {
        QString tline = line.trimmed();
        if (tline.length() <= 1)
        {
            // Empty chat command
            displayWarning(QCoreApplication::translate("server", "Unknown command or invalid parameters. Say '/help' in chat for a list of commands."));
            return true;
        }
        if (tline.startsWith("/me"))
            return false; // not a real command
        else if (tline == "/clear") {
            chatStrings.clear();
            chatText->clear();
        }
        else if (tline == "/discardStyleSheet")
            discardStyleSheet();
        else if (tline == "/saveStyleSheet")
            saveStyleSheet();
        else
            emit consoleCommand(tline.mid(1));

        return true;
    }

    return false;
}


void HWChatWidget::setUser(const QString & nickname)
{
    m_userNick = nickname;
    nickRemoved(nickname);
    clear();
}


void HWChatWidget::setUsersModel(QAbstractItemModel *model)
{
    chatNicks->selectionModel()->deleteLater();

    chatNicks->setModel(model);
    chatNicks->setModelColumn(0);

    QSortFilterProxyModel * sfpModel = qobject_cast<QSortFilterProxyModel *>(model);
    if (sfpModel)
        m_usersModel = qobject_cast<PlayersListModel*>(sfpModel->sourceModel());
    else
        m_usersModel = qobject_cast<PlayersListModel*>(model);
}

void HWChatWidget::nicksContextMenuRequested(const QPoint &pos)
{
    QModelIndexList mil = chatNicks->selectionModel()->selectedRows();

    QString nick;

    if(mil.size() == 0)
        return;
    else if(mil.size() == 1)
        nick = mil[0].data().toString();
    else
        nick = m_clickedNick;

    bool isOnline = (mil.size() > 0);

    QSortFilterProxyModel * playersSortFilterModel = qobject_cast<QSortFilterProxyModel *>(chatNicks->model());
    if(!playersSortFilterModel)
        return;

    PlayersListModel * players = qobject_cast<PlayersListModel *>(playersSortFilterModel->sourceModel());

    if(!players)
        return;

    bool isSelf = (nick == m_userNick);
    bool isInRoom = players->isFlagSet(nick, PlayersListModel::InRoom);

    acFollow->setVisible(!isSelf && isInRoom);

    acInfo->setVisible(isOnline);

    // update context menu labels according to possible action
    if(players->isFlagSet(nick, PlayersListModel::Ignore))
    {
        acIgnore->setText(QAction::tr("Unignore"));
        acIgnore->setIcon(QIcon(":/res/unignore.png"));
    }
    else
    {
        acIgnore->setText(QAction::tr("Ignore"));
        acIgnore->setIcon(QIcon(":/res/ignore.png"));
        acIgnore->setVisible(!isSelf);
    }

    if(players->isFlagSet(nick, PlayersListModel::Friend))
    {
        acFriend->setText(QAction::tr("Remove friend"));
        acFriend->setIcon(QIcon(":/res/remfriend.png"));
    }
    else
    {
        acFriend->setText(QAction::tr("Add friend"));
        acFriend->setIcon(QIcon(":/res/addfriend.png"));
        acFriend->setVisible(!isSelf);
    }

    if (m_isAdmin)
    {
        acKick->setVisible(!isSelf && isOnline);
        acBan->setVisible(!isSelf);
        acDelegate->setVisible(!isSelf && players->isFlagSet(m_userNick, PlayersListModel::InRoom));
    }

    m_nicksMenu->clear();

    foreach(QAction * action, chatNicks->actions())
        m_nicksMenu->addAction(action);

    m_nicksMenu->popup(chatNicks->mapToGlobal(pos));
}

void HWChatWidget::beforeContentAdd()
{
    m_scrollBarPos = chatText->verticalScrollBar()->value();
    m_scrollToBottom = m_scrollBarPos == chatText->verticalScrollBar()->maximum();
}

void HWChatWidget::afterContentAdd()
{
    if(m_scrollToBottom)
    {
        chatText->verticalScrollBar()->setValue(chatText->verticalScrollBar()->maximum());
        chatText->moveCursor(QTextCursor::End);
    } else
    {
        chatText->verticalScrollBar()->setValue(m_scrollBarPos);
    }
}

void HWChatWidget::resizeEvent(QResizeEvent * event)
{
    Q_UNUSED(event);

    afterContentAdd();
}

void HWChatWidget::showEvent(QShowEvent * event)
{
    Q_UNUSED(event);

     afterContentAdd();
}