X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fclient%2Fnetworkmodel.cpp;h=7b7f5bc458e0e71719e1c7bdc1459f17453b6afe;hp=10e7575fa119c9b46fafe68e42cd426bc8b80445;hb=3a3e844f9fcfd12235a0086af75ecd503b621ef4;hpb=9d54503555534a2c554f09a33df6afa33d6308ec diff --git a/src/client/networkmodel.cpp b/src/client/networkmodel.cpp index 10e7575f..7b7f5bc4 100644 --- a/src/client/networkmodel.cpp +++ b/src/client/networkmodel.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2005-2014 by the Quassel Project * + * Copyright (C) 2005-2018 by the Quassel Project * * devel@quassel-irc.org * * * * This program is free software; you can redistribute it and/or modify * @@ -22,7 +22,7 @@ #include #include -#include // for Qt::escape() +#include #include "buffermodel.h" #include "buffersettings.h" @@ -32,14 +32,15 @@ #include "ircchannel.h" #include "network.h" #include "signalproxy.h" +#include "buffersyncer.h" /***************************************** * Network Items *****************************************/ NetworkItem::NetworkItem(const NetworkId &netid, AbstractTreeItem *parent) - : PropertyMapItem(QList() << "networkName" << "currentServer" << "nickCount", parent), + : PropertyMapItem(parent), _networkId(netid), - _statusBufferItem(0) + _statusBufferItem(nullptr) { // DO NOT EMIT dataChanged() DIRECTLY IN NetworkItem // use networkDataChanged() instead. Otherwise you will end up in a infinite loop @@ -50,6 +51,13 @@ NetworkItem::NetworkItem(const NetworkId &netid, AbstractTreeItem *parent) } +QStringList NetworkItem::propertyOrder() const +{ + static QStringList order{"networkName", "currentServer", "nickCount"}; + return order; +} + + QVariant NetworkItem::data(int column, int role) const { switch (role) { @@ -72,11 +80,19 @@ QVariant NetworkItem::data(int column, int role) const } } +QString NetworkItem::escapeHTML(const QString &string, bool useNonbreakingSpaces) +{ + // QString.replace() doesn't guarantee the source string will remain constant. + // Use a local variable to avoid compiler errors. + QString formattedString = string.toHtmlEscaped(); + return (useNonbreakingSpaces ? formattedString.replace(" ", " ") : formattedString); +} + // FIXME shouldn't we check the bufferItemCache here? BufferItem *NetworkItem::findBufferItem(BufferId bufferId) { - BufferItem *bufferItem = 0; + BufferItem *bufferItem = nullptr; for (int i = 0; i < childCount(); i++) { bufferItem = qobject_cast(child(i)); @@ -85,7 +101,7 @@ BufferItem *NetworkItem::findBufferItem(BufferId bufferId) if (bufferItem->bufferId() == bufferId) return bufferItem; } - return 0; + return nullptr; } @@ -131,6 +147,14 @@ BufferItem *NetworkItem::bufferItem(const BufferInfo &bufferInfo) break; } + BufferSyncer *bufferSyncer = Client::bufferSyncer(); + if (bufferSyncer) { + bufferItem->addActivity( + bufferSyncer->activity(bufferItem->bufferId()), + bufferSyncer->highlightCount(bufferItem->bufferId()) > 0 + ); + } + return bufferItem; } @@ -153,7 +177,7 @@ void NetworkItem::attachNetwork(Network *network) connect(network, SIGNAL(connectedSet(bool)), this, SIGNAL(networkDataChanged())); connect(network, SIGNAL(destroyed()), - this, SIGNAL(networkDataChanged())); + this, SLOT(onNetworkDestroyed())); emit networkDataChanged(); } @@ -177,7 +201,7 @@ void NetworkItem::attachIrcChannel(IrcChannel *ircChannel) void NetworkItem::attachIrcUser(IrcUser *ircUser) { - QueryBufferItem *queryItem = 0; + QueryBufferItem *queryItem = nullptr; for (int i = 0; i < childCount(); i++) { queryItem = qobject_cast(child(i)); if (!queryItem) @@ -208,16 +232,31 @@ void NetworkItem::setCurrentServer(const QString &serverName) QString NetworkItem::toolTip(int column) const { Q_UNUSED(column); + QString strTooltip; + QTextStream tooltip( &strTooltip, QIODevice::WriteOnly ); + tooltip << ""; + + // Function to add a row to the tooltip table + auto addRow = [&](const QString& key, const QString& value, bool condition) { + if (condition) { + tooltip << "" << key << "" << value << ""; + } + }; - QStringList toolTip(QString("%1").arg(Qt::escape(networkName()))); - toolTip.append(tr("Server: %1").arg(Qt::escape(currentServer()))); - toolTip.append(tr("Users: %1").arg(nickCount())); - - if (_network) { - toolTip.append(tr("Lag: %1 msecs").arg(_network->latency())); + tooltip << "

" << NetworkItem::escapeHTML(networkName(), true) << "

"; + if (isActive()) { + tooltip << ""; + addRow(tr("Server"), NetworkItem::escapeHTML(currentServer(), true), !currentServer().isEmpty()); + addRow(tr("Users"), QString::number(nickCount()), true); + if (_network) + addRow(tr("Lag"), NetworkItem::escapeHTML(tr("%1 msecs").arg(_network->latency()), true), true); + + tooltip << "
"; + } else { + tooltip << "

" << tr("Not connected") << "

"; } - - return QString("

%1

").arg(toolTip.join("
")); + tooltip << "
"; + return strTooltip; } @@ -226,25 +265,40 @@ void NetworkItem::onBeginRemoveChilds(int start, int end) for (int i = start; i <= end; i++) { StatusBufferItem *statusBufferItem = qobject_cast(child(i)); if (statusBufferItem) { - _statusBufferItem = 0; + _statusBufferItem = nullptr; break; } } } +void NetworkItem::onNetworkDestroyed() +{ + _network = nullptr; + emit networkDataChanged(); + removeAllChilds(); +} + + /***************************************** * Fancy Buffer Items *****************************************/ -BufferItem::BufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent) - : PropertyMapItem(QStringList() << "bufferName" << "topic" << "nickCount", parent), - _bufferInfo(bufferInfo), +BufferItem::BufferItem(BufferInfo bufferInfo, AbstractTreeItem *parent) + : PropertyMapItem(parent), + _bufferInfo(std::move(bufferInfo)), _activity(BufferInfo::NoActivity) { setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled); } +QStringList BufferItem::propertyOrder() const +{ + static QStringList order{"bufferName", "topic", "nickCount"}; + return order; +} + + void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level) { if (_activity != level) { @@ -256,11 +310,16 @@ void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level) void BufferItem::clearActivityLevel() { - _activity = BufferInfo::NoActivity; + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) { + // If the core handles activity sync, clear only the highlight flag + _activity &= ~BufferInfo::Highlight; + } else { + _activity = BufferInfo::NoActivity; + } _firstUnreadMsgId = MsgId(); // FIXME remove with core proto v11 - if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) { _markerLineMsgId = _lastSeenMsgId; } @@ -270,6 +329,11 @@ void BufferItem::clearActivityLevel() void BufferItem::updateActivityLevel(const Message &msg) { + // If the core handles activity, and this message is not a highlight, ignore this + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) { + return; + } + if (isCurrentBuffer()) { return; } @@ -290,19 +354,43 @@ void BufferItem::updateActivityLevel(const Message &msg) _firstUnreadMsgId = msg.msgId(); } + Message::Types type; + // If the core handles activities, ignore types + if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) { + type = Message::Types(); + } else { + type = msg.type(); + } + + if (addActivity(type, msg.flags().testFlag(Message::Highlight)) || stateChanged) { + emit dataChanged(); + } +} + +void BufferItem::setActivity(Message::Types type, bool highlight) { BufferInfo::ActivityLevel oldLevel = activityLevel(); - _activity |= BufferInfo::OtherActivity; - if (msg.type() & (Message::Plain | Message::Notice | Message::Action)) + _activity &= BufferInfo::Highlight; + addActivity(type, highlight); + + if (_activity != oldLevel) { + emit dataChanged(); + } +} + +bool BufferItem::addActivity(Message::Types type, bool highlight) { + auto oldActivity = activityLevel(); + + if (type != Message::Types()) + _activity |= BufferInfo::OtherActivity; + + if (type.testFlag(Message::Plain) || type.testFlag(Message::Notice) || type.testFlag(Message::Action)) _activity |= BufferInfo::NewMessage; - if (msg.flags() & Message::Highlight) + if (highlight) _activity |= BufferInfo::Highlight; - stateChanged |= (oldLevel != _activity); - - if (stateChanged) - emit dataChanged(); + return oldActivity != _activity; } @@ -358,7 +446,7 @@ void BufferItem::setLastSeenMsgId(MsgId msgId) _lastSeenMsgId = msgId; // FIXME remove with core protocol v11 - if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) { + if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) { if (!isCurrentBuffer()) _markerLineMsgId = msgId; } @@ -411,7 +499,7 @@ QString StatusBufferItem::toolTip(int column) const *****************************************/ QueryBufferItem::QueryBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent) : BufferItem(bufferInfo, parent), - _ircUser(0) + _ircUser(nullptr) { setFlags(flags() | Qt::ItemIsDropEnabled | Qt::ItemIsEditable); @@ -448,6 +536,12 @@ bool QueryBufferItem::setData(int column, const QVariant &value, int role) case Qt::EditRole: { QString newName = value.toString(); + + // Sanity check - buffer names must not contain newlines! + int nlpos = newName.indexOf('\n'); + if (nlpos >= 0) + newName = newName.left(nlpos); + if (!newName.isEmpty()) { Client::renameBuffer(bufferId(), newName); return true; @@ -477,36 +571,120 @@ QString QueryBufferItem::toolTip(int column) const { // pretty much code duplication of IrcUserItem::toolTip() but inheritance won't solve this... Q_UNUSED(column); - QStringList toolTip; + QString strTooltip; + QTextStream tooltip( &strTooltip, QIODevice::WriteOnly ); + tooltip << ""; + + // Keep track of whether or not information has been added + bool infoAdded = false; + + // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem + tooltip << "

"; + tooltip << tr("Query with %1").arg(NetworkItem::escapeHTML(bufferName(), true)); + if (!_ircUser) { + // User seems to be offline, let the no information message be added below + tooltip << "

"; + } else { + // Function to add a row to the tooltip table + auto addRow = [&](const QString& key, const QString& value, bool condition) { + if (condition) { + tooltip << "" << key << "" << value << ""; + infoAdded = true; + } + }; - toolTip.append(tr("Query with %1").arg(bufferName())); + // User information is available + if (_ircUser->userModes() != "") { + //TODO Translate user Modes and add them to the table below and in IrcUserItem::toolTip + tooltip << " (" << _ircUser->userModes() << ")"; + } + tooltip << "

"; - if (_ircUser) { - if (_ircUser->userModes() != "") toolTip[0].append(QString(" (+%1)").arg(_ircUser->userModes())); + tooltip << ""; if (_ircUser->isAway()) { - toolTip[0].append(QString(" (away%1)").arg(!_ircUser->awayMessage().isEmpty() ? (QString(" ") + _ircUser->awayMessage()) : QString())); + QString awayMessageHTML = QString("

%1

").arg(tr("Unknown")); + + // If away message is known, replace with the escaped message. + if (!_ircUser->awayMessage().isEmpty()) { + awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage()); + } + addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true); + } + addRow(tr("Realname"), + NetworkItem::escapeHTML(_ircUser->realName()), + !_ircUser->realName().isEmpty()); + // suserHost may return " is available for help", which should be translated. + // See https://www.alien.net.au/irc/irc2numerics.html + if(_ircUser->suserHost().endsWith("available for help")) { + addRow(NetworkItem::escapeHTML(tr("Help status"), true), + NetworkItem::escapeHTML(tr("Available for help")), + true); + } else { + addRow(NetworkItem::escapeHTML(tr("Service status"), true), + NetworkItem::escapeHTML(_ircUser->suserHost()), + !_ircUser->suserHost().isEmpty()); } - if (!_ircUser->realName().isEmpty()) toolTip.append(_ircUser->realName()); - if (!_ircUser->ircOperator().isEmpty()) toolTip.append(QString("%1 %2").arg(_ircUser->nick()).arg(_ircUser->ircOperator())); - if (!_ircUser->suserHost().isEmpty()) toolTip.append(_ircUser->suserHost()); - if (!_ircUser->whoisServiceReply().isEmpty()) toolTip.append(_ircUser->whoisServiceReply()); - toolTip.append(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!")+1)); + // Keep track of whether or not the account information's been added. Don't show it twice. + bool accountAdded = false; + if(!_ircUser->account().isEmpty()) { + // IRCv3 account-notify is supported by the core and IRC server. + // Assume logged out (seems to be more common) + QString accountHTML = QString("

%1

").arg(tr("Not logged in")); + + // If account is logged in, replace with the escaped account name. + if (_ircUser->account() != "*") { + accountHTML = NetworkItem::escapeHTML(_ircUser->account()); + } + addRow(NetworkItem::escapeHTML(tr("Account"), true), + accountHTML, + true); + // Mark the row as added + accountAdded = true; + } + // whoisServiceReply may return " is identified for this nick", which should be translated. + // See https://www.alien.net.au/irc/irc2numerics.html + if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) { + addRow(NetworkItem::escapeHTML(tr("Account"), true), + NetworkItem::escapeHTML(tr("Identified for this nick")), + !accountAdded); + // Don't add the account row again if information's already added via account-notify + // Not used further down... + // accountAdded = true; + } else { + addRow(NetworkItem::escapeHTML(tr("Service Reply"), true), + NetworkItem::escapeHTML(_ircUser->whoisServiceReply()), + !_ircUser->whoisServiceReply().isEmpty()); + } + addRow(tr("Hostmask"), + NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)), + !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@")); + // ircOperator may contain "is an" or "is a", which should be removed. + addRow(tr("Operator"), + NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")), + !_ircUser->ircOperator().isEmpty()); if (_ircUser->idleTime().isValid()) { QDateTime now = QDateTime::currentDateTime(); QDateTime idle = _ircUser->idleTime(); int idleTime = idle.secsTo(now); - toolTip.append(tr("idling since %1").arg(secondsToString(idleTime))); + addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true); } + if (_ircUser->loginTime().isValid()) { - toolTip.append(tr("login time: %1").arg(_ircUser->loginTime().toString())); + addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true); } - if (!_ircUser->server().isEmpty()) toolTip.append(tr("server: %1").arg(_ircUser->server())); + addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty()); + tooltip << "
"; } - return QString("

%1

").arg(toolTip.join("
")); + // If no further information found, offer an explanatory message + if (!infoAdded) + tooltip << "

" << tr("No information available") << "

"; + + tooltip << "
"; + return strTooltip; } @@ -516,10 +694,11 @@ void QueryBufferItem::setIrcUser(IrcUser *ircUser) return; if (_ircUser) { - disconnect(_ircUser, 0, this, 0); + disconnect(_ircUser, nullptr, this, nullptr); } if (ircUser) { + connect(ircUser, SIGNAL(destroyed(QObject*)), SLOT(removeIrcUser())); connect(ircUser, SIGNAL(quited()), this, SLOT(removeIrcUser())); connect(ircUser, SIGNAL(awaySet(bool)), this, SIGNAL(dataChanged())); connect(ircUser, SIGNAL(encryptedSet(bool)), this, SLOT(setEncrypted(bool))); @@ -532,8 +711,22 @@ void QueryBufferItem::setIrcUser(IrcUser *ircUser) void QueryBufferItem::removeIrcUser() { - _ircUser = 0; - emit dataChanged(); + if (_ircUser) { + // Disconnect the active IrcUser before removing it, otherwise it will fire removeIrcUser() + // a second time when the object's destroyed due to QueryBufferItem::setIrcUser() connecting + // SIGNAL destroyed(QObject*) to SLOT removeIrcUser(). + // This fixes removing an active IrcUser if the user had quit then rejoined in a nonstandard + // manner (e.g. updateNickFromHost calling newIrcUser, triggered by an away-notify message). + disconnect(_ircUser, nullptr, this, nullptr); + + // Clear IrcUser (only set to 0 if not already 0) + _ircUser = nullptr; + + // Only emit dataChanged() if data actually changed. This might serve as a small + // optimization, but it can be moved outside the if statement if other behavior depends on + // it always being called. + emit dataChanged(); + } } @@ -542,8 +735,9 @@ void QueryBufferItem::removeIrcUser() *****************************************/ ChannelBufferItem::ChannelBufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent) : BufferItem(bufferInfo, parent), - _ircChannel(0) + _ircChannel(nullptr) { + setFlags(flags() | Qt::ItemIsDropEnabled); } @@ -561,16 +755,28 @@ QVariant ChannelBufferItem::data(int column, int role) const QString ChannelBufferItem::toolTip(int column) const { Q_UNUSED(column); - QStringList toolTip; + QString strTooltip; + QTextStream tooltip( &strTooltip, QIODevice::WriteOnly ); + tooltip << ""; + + // Function to add a row to the tooltip table + auto addRow = [&](const QString& key, const QString& value, bool condition) { + if (condition) { + tooltip << "" << key << "" << value << ""; + } + }; + + tooltip << "

"; + tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "

"; - toolTip.append(tr("Channel %1").arg(Qt::escape(bufferName()))); if (isActive()) { - //TODO: add channel modes - toolTip.append(tr("Users: %1").arg(nickCount())); + tooltip << ""; + addRow(tr("Users"), QString::number(nickCount()), true); + if (_ircChannel) { QString channelMode = _ircChannel->channelModeString(); // channelModeString is compiled on the fly -> thus cache the result if (!channelMode.isEmpty()) - toolTip.append(tr("Mode: %1").arg(channelMode)); + addRow(tr("Mode"), channelMode, true); } ItemViewSettings s; @@ -579,26 +785,32 @@ QString ChannelBufferItem::toolTip(int column) const QString _topic = topic(); if (_topic != "") { _topic = stripFormatCodes(_topic); - _topic = Qt::escape(_topic); - toolTip.append(QString(" ")); - toolTip.append(tr("Topic: %1").arg(_topic)); + _topic = NetworkItem::escapeHTML(_topic); + addRow(tr("Topic"), _topic, true); } } - } - else { - toolTip.append(tr("Not active
Double-click to join")); + + tooltip << "
"; + } else { + tooltip << "

" << tr("Not active, double-click to join") << "

"; } - return tr("

%1

").arg(toolTip.join("
")); + tooltip << "
"; + return strTooltip; } void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel) { - Q_ASSERT(!_ircChannel && ircChannel); + if (_ircChannel) { + qWarning() << Q_FUNC_INFO << "IrcChannel already set; cleanup failed!?"; + disconnect(_ircChannel, nullptr, this, nullptr); + } _ircChannel = ircChannel; + connect(ircChannel, SIGNAL(destroyed(QObject*)), + this, SLOT(ircChannelDestroyed())); connect(ircChannel, SIGNAL(topicSet(QString)), this, SLOT(setTopic(QString))); connect(ircChannel, SIGNAL(encryptedSet(bool)), @@ -622,17 +834,37 @@ void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel) emit dataChanged(); } +QString ChannelBufferItem::nickChannelModes(const QString &nick) const +{ + if (!_ircChannel) { + qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes"; + return QString(); + } + + return _ircChannel->userModes(nick); +} + void ChannelBufferItem::ircChannelParted() { Q_CHECK_PTR(_ircChannel); - disconnect(_ircChannel, 0, this, 0); - _ircChannel = 0; + disconnect(_ircChannel, nullptr, this, nullptr); + _ircChannel = nullptr; emit dataChanged(); removeAllChilds(); } +void ChannelBufferItem::ircChannelDestroyed() +{ + if (_ircChannel) { + _ircChannel = nullptr; + emit dataChanged(); + removeAllChilds(); + } +} + + void ChannelBufferItem::join(const QList &ircUsers) { addUsersToCategory(ircUsers); @@ -642,7 +874,7 @@ void ChannelBufferItem::join(const QList &ircUsers) UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId) { - UserCategoryItem *categoryItem = 0; + UserCategoryItem *categoryItem = nullptr; for (int i = 0; i < childCount(); i++) { categoryItem = qobject_cast(child(i)); @@ -651,7 +883,7 @@ UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId) if (categoryItem->categoryId() == categoryId) return categoryItem; } - return 0; + return nullptr; } @@ -668,7 +900,7 @@ void ChannelBufferItem::addUsersToCategory(const QList &ircUsers) QHash > categories; int categoryId = -1; - UserCategoryItem *categoryItem = 0; + UserCategoryItem *categoryItem = nullptr; foreach(IrcUser *ircUser, ircUsers) { categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser)); @@ -684,7 +916,7 @@ void ChannelBufferItem::addUsersToCategory(const QList &ircUsers) QHash >::const_iterator catIter = categories.constBegin(); while (catIter != categories.constEnd()) { catIter.key()->addUsers(catIter.value()); - catIter++; + ++catIter; } } @@ -696,7 +928,7 @@ void ChannelBufferItem::part(IrcUser *ircUser) return; } - disconnect(ircUser, 0, this, 0); + disconnect(ircUser, nullptr, this, nullptr); removeUserFromCategory(ircUser); emit dataChanged(2); } @@ -711,7 +943,7 @@ void ChannelBufferItem::removeUserFromCategory(IrcUser *ircUser) return; } - UserCategoryItem *categoryItem = 0; + UserCategoryItem *categoryItem = nullptr; for (int i = 0; i < childCount(); i++) { categoryItem = qobject_cast(child(i)); if (categoryItem->removeUser(ircUser)) { @@ -741,7 +973,7 @@ void ChannelBufferItem::userModeChanged(IrcUser *ircUser) } // find the item that needs reparenting - IrcUserItem *ircUserItem = 0; + IrcUserItem *ircUserItem = nullptr; for (int i = 0; i < childCount(); i++) { UserCategoryItem *oldCategoryItem = qobject_cast(child(i)); Q_ASSERT(oldCategoryItem); @@ -768,7 +1000,7 @@ void ChannelBufferItem::userModeChanged(IrcUser *ircUser) const QList UserCategoryItem::categories = QList() << 'q' << 'a' << 'o' << 'h' << 'v'; UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent) - : PropertyMapItem(QStringList() << "categoryName", parent), + : PropertyMapItem(parent), _category(category) { setFlags(Qt::ItemIsEnabled); @@ -777,30 +1009,37 @@ UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent) } +QStringList UserCategoryItem::propertyOrder() const +{ + static QStringList order{"categoryName"}; + return order; +} + + // caching this makes no sense, since we display the user number dynamically QString UserCategoryItem::categoryName() const { int n = childCount(); switch (_category) { case 0: - return tr("%n Owner(s)", 0, n); + return tr("%n Owner(s)", "", n); case 1: - return tr("%n Admin(s)", 0, n); + return tr("%n Admin(s)", "", n); case 2: - return tr("%n Operator(s)", 0, n); + return tr("%n Operator(s)", "", n); case 3: - return tr("%n Half-Op(s)", 0, n); + return tr("%n Half-Op(s)", "", n); case 4: - return tr("%n Voiced", 0, n); + return tr("%n Voiced", "", n); default: - return tr("%n User(s)", 0, n); + return tr("%n User(s)", "", n); } } IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser) { - IrcUserItem *userItem = 0; + IrcUserItem *userItem = nullptr; for (int i = 0; i < childCount(); i++) { userItem = qobject_cast(child(i)); @@ -809,7 +1048,7 @@ IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser) if (userItem->ircUser() == ircUser) return userItem; } - return 0; + return nullptr; } @@ -870,7 +1109,7 @@ QVariant UserCategoryItem::data(int column, int role) const * Irc User Items *****************************************/ IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent) - : PropertyMapItem(QStringList() << "nickName", parent), + : PropertyMapItem(parent), _ircUser(ircUser) { setObjectName(ircUser->nick()); @@ -880,6 +1119,13 @@ IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent) } +QStringList IrcUserItem::propertyOrder() const +{ + static QStringList order{"nickName"}; + return order; +} + + QVariant IrcUserItem::data(int column, int role) const { switch (role) { @@ -908,33 +1154,132 @@ QVariant IrcUserItem::data(int column, int role) const QString IrcUserItem::toolTip(int column) const { Q_UNUSED(column); - QStringList toolTip(QString("%1").arg(nickName())); - if (_ircUser->userModes() != "") toolTip[0].append(QString(" (%1)").arg(_ircUser->userModes())); + QString strTooltip; + QTextStream tooltip( &strTooltip, QIODevice::WriteOnly ); + tooltip << ""; + + // Keep track of whether or not information has been added + bool infoAdded = false; + + // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem + tooltip << "

" << NetworkItem::escapeHTML(nickName(), true); + if (_ircUser->userModes() != "") { + //TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip + tooltip << " (" << _ircUser->userModes() << ")"; + } + tooltip << "

"; + + auto addRow = [&](const QString& key, const QString& value, bool condition) { + if (condition) + { + tooltip << "" << key << "" << value << ""; + infoAdded = true; + } + }; + + tooltip << ""; + addRow(tr("Modes"), + NetworkItem::escapeHTML(channelModes()), + !channelModes().isEmpty()); if (_ircUser->isAway()) { - toolTip[0].append(tr(" is away")); - if (!_ircUser->awayMessage().isEmpty()) - toolTip[0].append(QString(" (%1)").arg(_ircUser->awayMessage())); + QString awayMessageHTML = QString("

%1

").arg(tr("Unknown")); + + // If away message is known, replace with the escaped message. + if (!_ircUser->awayMessage().isEmpty()) { + awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage()); + } + addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true); + } + addRow(tr("Realname"), + NetworkItem::escapeHTML(_ircUser->realName()), + !_ircUser->realName().isEmpty()); + + // suserHost may return " is available for help", which should be translated. + // See https://www.alien.net.au/irc/irc2numerics.html + if(_ircUser->suserHost().endsWith("available for help")) { + addRow(NetworkItem::escapeHTML(tr("Help status"), true), + NetworkItem::escapeHTML(tr("Available for help")), + true); + } else { + addRow(NetworkItem::escapeHTML(tr("Service status"), true), + NetworkItem::escapeHTML(_ircUser->suserHost()), + !_ircUser->suserHost().isEmpty()); } - if (!_ircUser->realName().isEmpty()) toolTip.append(_ircUser->realName()); - if (!_ircUser->ircOperator().isEmpty()) toolTip.append(QString("%1 %2").arg(nickName()).arg(_ircUser->ircOperator())); - if (!_ircUser->suserHost().isEmpty()) toolTip.append(_ircUser->suserHost()); - if (!_ircUser->whoisServiceReply().isEmpty()) toolTip.append(_ircUser->whoisServiceReply()); - toolTip.append(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!")+1)); + // Keep track of whether or not the account information's been added. Don't show it twice. + bool accountAdded = false; + if(!_ircUser->account().isEmpty()) { + // IRCv3 account-notify is supported by the core and IRC server. + // Assume logged out (seems to be more common) + QString accountHTML = QString("

%1

").arg(tr("Not logged in")); + + // If account is logged in, replace with the escaped account name. + if (_ircUser->account() != "*") { + accountHTML = NetworkItem::escapeHTML(_ircUser->account()); + } + addRow(NetworkItem::escapeHTML(tr("Account"), true), + accountHTML, + true); + // Mark the row as added + accountAdded = true; + } + // whoisServiceReply may return " is identified for this nick", which should be translated. + // See https://www.alien.net.au/irc/irc2numerics.html + if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) { + addRow(NetworkItem::escapeHTML(tr("Account"), true), + NetworkItem::escapeHTML(tr("Identified for this nick")), + !accountAdded); + // Don't add the account row again if information's already added via account-notify + // Not used further down... + // accountAdded = true; + } else { + addRow(NetworkItem::escapeHTML(tr("Service Reply"), true), + NetworkItem::escapeHTML(_ircUser->whoisServiceReply()), + !_ircUser->whoisServiceReply().isEmpty()); + } + addRow(tr("Hostmask"), + NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)), + !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@")); + // ircOperator may contain "is an" or "is a", which should be removed. + addRow(tr("Operator"), + NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")), + !_ircUser->ircOperator().isEmpty()); if (_ircUser->idleTime().isValid()) { QDateTime now = QDateTime::currentDateTime(); QDateTime idle = _ircUser->idleTime(); int idleTime = idle.secsTo(now); - toolTip.append(tr("idling since %1").arg(secondsToString(idleTime))); + addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true); } + if (_ircUser->loginTime().isValid()) { - toolTip.append(tr("login time: %1").arg(_ircUser->loginTime().toString())); + addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true); } - if (!_ircUser->server().isEmpty()) toolTip.append(tr("server: %1").arg(_ircUser->server())); + addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty()); + tooltip << "
"; - return QString("

%1

").arg(toolTip.join("
")); + // If no further information found, offer an explanatory message + if (!infoAdded) + tooltip << "

" << tr("No information available") << "

"; + + tooltip << "
"; + return strTooltip; +} + +QString IrcUserItem::channelModes() const +{ + // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem. + // We want the channel buffer item in order to get the channel-specific user modes. + UserCategoryItem *category = qobject_cast(parent()); + if (!category) + return QString(); + + ChannelBufferItem *channel = qobject_cast(category->parent()); + if (!channel) + return QString(); + + return channel->nickChannelModes(nickName()); } @@ -973,7 +1318,7 @@ bool NetworkModel::isBufferIndex(const QModelIndex &index) const int NetworkModel::networkRow(NetworkId networkId) const { - NetworkItem *netItem = 0; + NetworkItem *netItem = nullptr; for (int i = 0; i < rootItem->childCount(); i++) { netItem = qobject_cast(rootItem->child(i)); if (!netItem) @@ -999,7 +1344,7 @@ NetworkItem *NetworkModel::findNetworkItem(NetworkId networkId) const { int netRow = networkRow(networkId); if (netRow == -1) - return 0; + return nullptr; else return qobject_cast(rootItem->child(netRow)); } @@ -1009,7 +1354,7 @@ NetworkItem *NetworkModel::networkItem(NetworkId networkId) { NetworkItem *netItem = findNetworkItem(networkId); - if (netItem == 0) { + if (netItem == nullptr) { netItem = new NetworkItem(networkId, rootItem); rootItem->newChild(netItem); } @@ -1040,7 +1385,7 @@ BufferItem *NetworkModel::findBufferItem(BufferId bufferId) const if (_bufferItemCache.contains(bufferId)) return _bufferItemCache[bufferId]; else - return 0; + return nullptr; } @@ -1078,7 +1423,7 @@ QList > NetworkModel::mimeDataToBufferList(const QMim if (!mimeContainsBufferList(mimeData)) return bufferList; - QStringList rawBufferList = QString::fromAscii(mimeData->data("application/Quassel/BufferItemList")).split(","); + QStringList rawBufferList = QString::fromLatin1(mimeData->data("application/Quassel/BufferItemList")).split(","); NetworkId networkId; BufferId bufferUid; foreach(QString rawBuffer, rawBufferList) { @@ -1106,7 +1451,7 @@ QMimeData *NetworkModel::mimeData(const QModelIndexList &indexes) const bufferlist << bufferid; } - mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toAscii()); + mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toLatin1()); return mimeData; } @@ -1219,7 +1564,7 @@ void NetworkModel::updateBufferActivity(Message &msg) case Message::Action: if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) { const Network *net = Client::network(msg.bufferInfo().networkId()); - IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : 0; + IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : nullptr; if (user) user->setLastChannelActivity(msg.bufferId(), msg.timestamp()); } @@ -1240,7 +1585,8 @@ void NetworkModel::updateBufferActivity(Message &msg) } } else { - updateBufferActivity(bufferItem(msg.bufferInfo()), msg); + if ((BufferSettings(msg.bufferId()).messageFilter() & msg.type()) != msg.type()) + updateBufferActivity(bufferItem(msg.bufferInfo()), msg); } } @@ -1282,7 +1628,7 @@ const Network *NetworkModel::networkByIndex(const QModelIndex &index) const { QVariant netVariant = index.data(NetworkIdRole); if (!netVariant.isValid()) - return 0; + return nullptr; NetworkId networkId = netVariant.value(); return Client::network(networkId); @@ -1425,3 +1771,24 @@ void NetworkModel::messageRedirectionSettingsChanged() _serverNoticesTarget = bufferSettings.serverNoticesTarget(); _errorMsgsTarget = bufferSettings.errorMsgsTarget(); } + +void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity) { + auto _bufferItem = findBufferItem(bufferId); + if (!_bufferItem) { + qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId; + return; + } + auto hiddenTypes = BufferSettings(bufferId).messageFilter(); + auto visibleTypes = ~hiddenTypes; + auto activityVisibleTypesIntersection = activity & visibleTypes; + _bufferItem->setActivity(activityVisibleTypesIntersection, false); +} + +void NetworkModel::highlightCountChanged(BufferId bufferId, int count) { + auto _bufferItem = findBufferItem(bufferId); + if (!_bufferItem) { + qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId; + return; + } + _bufferItem->addActivity(Message::Types{}, count > 0); +}