Hide network tooltip info when disconnected
[quassel.git] / src / client / networkmodel.cpp
index c85f579..bba2175 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2015 by the Quassel Project                        *
+ *   Copyright (C) 2005-2016 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -74,6 +74,18 @@ QVariant NetworkItem::data(int column, int role) const
     }
 }
 
+QString NetworkItem::escapeHTML(const QString &string, bool useNonbreakingSpaces)
+{
+    // QString.replace() doesn't guarentee the source string will remain constant.
+    // Use a local variable to avoid compiler errors.
+#if QT_VERSION < 0x050000
+    QString formattedString = Qt::escape(string);
+#else
+    QString formattedString = string.toHtmlEscaped();
+#endif
+    return (useNonbreakingSpaces ? formattedString.replace(" ", "&nbsp;") : formattedString);
+}
+
 
 // FIXME shouldn't we check the bufferItemCache here?
 BufferItem *NetworkItem::findBufferItem(BufferId bufferId)
@@ -210,21 +222,31 @@ void NetworkItem::setCurrentServer(const QString &serverName)
 QString NetworkItem::toolTip(int column) const
 {
     Q_UNUSED(column);
+    QString strTooltip;
+    QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
+    tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
+
+    // Function to add a row to the tooltip table
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
+        if (condition) {
+            tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+        }
+    };
 
-#if QT_VERSION < 0x050000
-    QStringList toolTip(QString("<b>%1</b>").arg(Qt::escape(networkName())));
-    toolTip.append(tr("Server: %1").arg(Qt::escape(currentServer())));
-#else
-    QStringList toolTip(QString("<b>%1</b>").arg(networkName().toHtmlEscaped()));
-    toolTip.append(tr("Server: %1").arg(currentServer().toHtmlEscaped()));
-#endif
-    toolTip.append(tr("Users: %1").arg(nickCount()));
+    tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(networkName(), true) << "</p>";
+    if (isActive()) {
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
+        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);
 
-    if (_network) {
-        toolTip.append(tr("Lag: %1 msecs").arg(_network->latency()));
+        tooltip << "</table>";
+    } else {
+        tooltip << "<p class='italic' align='center'>" << tr("Not connected") << "</p>";
     }
-
-    return QString("<p> %1 </p>").arg(toolTip.join("<br />"));
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -463,6 +485,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;
@@ -492,36 +520,119 @@ 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 << "<qt><style>.bold { font-weight: bold; }</style>"
+            << "<style>.italic { font-style: italic; }</style>";
+
+    // Keep track of whether or not information has been added
+    bool infoAdded = false;
+
+    // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
+    tooltip << "<p class='bold' align='center'>";
+    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 << "</p>";
+    } else {
+        // Function to add a row to the tooltip table
+        auto addRow = [&](const QString& key, const QString& value, bool condition) {
+            if (condition) {
+                tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+                infoAdded = true;
+            }
+        };
 
-    toolTip.append(tr("<b>Query with %1</b>").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 << "</p>";
 
-    if (_ircUser) {
-        if (_ircUser->userModes() != "") toolTip[0].append(QString(" (+%1)").arg(_ircUser->userModes()));
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
         if (_ircUser->isAway()) {
-            toolTip[0].append(QString(" (away%1)").arg(!_ircUser->awayMessage().isEmpty() ? (QString(" ") + _ircUser->awayMessage()) : QString()));
+            QString awayMessage(tr("(unknown)"));
+            if(!_ircUser->awayMessage().isEmpty()) {
+                awayMessage = _ircUser->awayMessage();
+            }
+            addRow(NetworkItem::escapeHTML(tr("Away message"), true), NetworkItem::escapeHTML(awayMessage), true);
+        }
+        addRow(tr("Realname"),
+               NetworkItem::escapeHTML(_ircUser->realName()),
+               !_ircUser->realName().isEmpty());
+        // suserHost may return "<nick> 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("<p class='italic'>%1</p>").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 "<nick> 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
+            // Mark the row as added
+            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 << "</table>";
     }
 
-    return QString("<p> %1 </p>").arg(toolTip.join("<br />"));
+    // If no further information found, offer an explanatory message
+    if (!infoAdded)
+        tooltip << "<p class='italic'>" << tr("No information available") << "</p>";
+
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -577,20 +688,29 @@ 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 << "<qt><style>.bold { font-weight: bold; }</style>"
+            << "<qt><style>.italic { font-style: italic; }</style>";
+
+    // Function to add a row to the tooltip table
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
+        if (condition) {
+            tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+        }
+    };
+
+    tooltip << "<p class='bold' align='center'>";
+    tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "</p>";
 
-#if QT_VERSION < 0x050000
-    toolTip.append(tr("<b>Channel %1</b>").arg(Qt::escape(bufferName())));
-#else
-    toolTip.append(tr("<b>Channel %1</b>").arg(bufferName().toHtmlEscaped()));
-#endif
     if (isActive()) {
-        //TODO: add channel modes
-        toolTip.append(tr("<b>Users:</b> %1").arg(nickCount()));
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
+        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("<b>Mode:</b> %1").arg(channelMode));
+                addRow(tr("Mode"), channelMode, true);
         }
 
         ItemViewSettings s;
@@ -599,21 +719,18 @@ QString ChannelBufferItem::toolTip(int column) const
             QString _topic = topic();
             if (_topic != "") {
                 _topic = stripFormatCodes(_topic);
-#if QT_VERSION < 0x050000
-                _topic = Qt::escape(_topic);
-#else
-                _topic = _topic.toHtmlEscaped();
-#endif
-                toolTip.append(QString("<font size='-2'>&nbsp;</font>"));
-                toolTip.append(tr("<b>Topic:</b> %1").arg(_topic));
+                _topic = NetworkItem::escapeHTML(_topic);
+                addRow(tr("Topic"), _topic, true);
             }
         }
-    }
-    else {
-        toolTip.append(tr("Not active <br /> Double-click to join"));
+
+        tooltip << "</table>";
+    } else {
+        tooltip << "<p class='italic'>" << tr("Not active, double-click to join") << "</p>";
     }
 
-    return tr("<p> %1 </p>").arg(toolTip.join("<br />"));
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -651,6 +768,16 @@ 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()
 {
@@ -947,33 +1074,131 @@ QVariant IrcUserItem::data(int column, int role) const
 QString IrcUserItem::toolTip(int column) const
 {
     Q_UNUSED(column);
-    QStringList toolTip(QString("<b>%1</b>").arg(nickName()));
-    if (_ircUser->userModes() != "") toolTip[0].append(QString(" (%1)").arg(_ircUser->userModes()));
-    if (_ircUser->isAway()) {
-        toolTip[0].append(tr(" is away"));
-        if (!_ircUser->awayMessage().isEmpty())
-            toolTip[0].append(QString(" (%1)").arg(_ircUser->awayMessage()));
-    }
-    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());
+    QString strTooltip;
+    QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
+    tooltip << "<qt><style>.bold { font-weight: bold; }</style>"
+            << "<style>.italic { font-style: italic; }</style>";
+
+    // Keep track of whether or not information has been added
+    bool infoAdded = false;
+
+    // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
+    tooltip << "<p class='bold' align='center'>" << 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 << "</p>";
+
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
+        if (condition)
+        {
+            tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+            infoAdded = true;
+        }
+    };
 
-    toolTip.append(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!")+1));
+    tooltip << "<table cellspacing='5' cellpadding='0'>";
+    addRow(tr("Modes"),
+           NetworkItem::escapeHTML(channelModes()),
+           !channelModes().isEmpty());
+    if (_ircUser->isAway()) {
+        QString awayMessage(tr("(unknown)"));
+        if(!_ircUser->awayMessage().isEmpty()) {
+            awayMessage = _ircUser->awayMessage();
+        }
+        addRow(NetworkItem::escapeHTML(tr("Away message"), true), NetworkItem::escapeHTML(awayMessage), true);
+    }
+    addRow(tr("Realname"),
+           NetworkItem::escapeHTML(_ircUser->realName()),
+           !_ircUser->realName().isEmpty());
+
+    // suserHost may return "<nick> 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());
+    }
+
+    // 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("<p class='italic'>%1</p>").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 "<nick> 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
+        // Mark the row as added
+        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 << "</table>";
+
+    // If no further information found, offer an explanatory message
+    if (!infoAdded)
+        tooltip << "<p class='italic'>" << tr("No information available") << "</p>";
+
+    tooltip << "</qt>";
+    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<UserCategoryItem *>(parent());
+    if (!category)
+        return QString();
+
+    ChannelBufferItem *channel = qobject_cast<ChannelBufferItem *>(category->parent());
+    if (!channel)
+        return QString();
 
-    return QString("<p> %1 </p>").arg(toolTip.join("<br />"));
+    return channel->nickChannelModes(nickName());
 }