0907deeac1e4561cf656e08f66eed654ccf56ef9
[quassel.git] / src / client / networkmodel.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "networkmodel.h"
22
23 #include <QAbstractItemView>
24 #include <QMimeData>
25 #if QT_VERSION < 0x050000
26 #include <QTextDocument>        // for Qt::escape()
27 #endif
28
29 #include "buffermodel.h"
30 #include "buffersettings.h"
31 #include "client.h"
32 #include "clientignorelistmanager.h"
33 #include "clientsettings.h"
34 #include "ircchannel.h"
35 #include "network.h"
36 #include "signalproxy.h"
37 #include "buffersyncer.h"
38
39 /*****************************************
40 *  Network Items
41 *****************************************/
42 NetworkItem::NetworkItem(const NetworkId &netid, AbstractTreeItem *parent)
43     : PropertyMapItem(parent),
44     _networkId(netid),
45     _statusBufferItem(0)
46 {
47     // DO NOT EMIT dataChanged() DIRECTLY IN NetworkItem
48     // use networkDataChanged() instead. Otherwise you will end up in a infinite loop
49     // as we "sync" the dataChanged() signals of NetworkItem and StatusBufferItem
50     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
51     connect(this, SIGNAL(networkDataChanged(int)), this, SIGNAL(dataChanged(int)));
52     connect(this, SIGNAL(beginRemoveChilds(int, int)), this, SLOT(onBeginRemoveChilds(int, int)));
53 }
54
55
56 QStringList NetworkItem::propertyOrder() const
57 {
58     static QStringList order{"networkName", "currentServer", "nickCount"};
59     return order;
60 }
61
62
63 QVariant NetworkItem::data(int column, int role) const
64 {
65     switch (role) {
66     case NetworkModel::BufferIdRole:
67     case NetworkModel::BufferInfoRole:
68     case NetworkModel::BufferTypeRole:
69     case NetworkModel::BufferActivityRole:
70         if (_statusBufferItem)
71             return _statusBufferItem->data(column, role);
72         else
73             return QVariant();
74     case NetworkModel::NetworkIdRole:
75         return qVariantFromValue(_networkId);
76     case NetworkModel::ItemTypeRole:
77         return NetworkModel::NetworkItemType;
78     case NetworkModel::ItemActiveRole:
79         return isActive();
80     default:
81         return PropertyMapItem::data(column, role);
82     }
83 }
84
85 QString NetworkItem::escapeHTML(const QString &string, bool useNonbreakingSpaces)
86 {
87     // QString.replace() doesn't guarantee the source string will remain constant.
88     // Use a local variable to avoid compiler errors.
89 #if QT_VERSION < 0x050000
90     QString formattedString = Qt::escape(string);
91 #else
92     QString formattedString = string.toHtmlEscaped();
93 #endif
94     return (useNonbreakingSpaces ? formattedString.replace(" ", "&nbsp;") : formattedString);
95 }
96
97
98 // FIXME shouldn't we check the bufferItemCache here?
99 BufferItem *NetworkItem::findBufferItem(BufferId bufferId)
100 {
101     BufferItem *bufferItem = 0;
102
103     for (int i = 0; i < childCount(); i++) {
104         bufferItem = qobject_cast<BufferItem *>(child(i));
105         if (!bufferItem)
106             continue;
107         if (bufferItem->bufferId() == bufferId)
108             return bufferItem;
109     }
110     return 0;
111 }
112
113
114 BufferItem *NetworkItem::bufferItem(const BufferInfo &bufferInfo)
115 {
116     BufferItem *bufferItem = findBufferItem(bufferInfo);
117     if (bufferItem)
118         return bufferItem;
119
120     switch (bufferInfo.type()) {
121     case BufferInfo::StatusBuffer:
122         _statusBufferItem = new StatusBufferItem(bufferInfo, this);
123         bufferItem = _statusBufferItem;
124         disconnect(this, SIGNAL(networkDataChanged(int)), this, SIGNAL(dataChanged(int)));
125         connect(this, SIGNAL(networkDataChanged(int)), bufferItem, SIGNAL(dataChanged(int)));
126         connect(bufferItem, SIGNAL(dataChanged(int)), this, SIGNAL(dataChanged(int)));
127         break;
128     case BufferInfo::ChannelBuffer:
129         bufferItem = new ChannelBufferItem(bufferInfo, this);
130         break;
131     case BufferInfo::QueryBuffer:
132         bufferItem = new QueryBufferItem(bufferInfo, this);
133         break;
134     default:
135         bufferItem = new BufferItem(bufferInfo, this);
136     }
137
138     newChild(bufferItem);
139
140     // postprocess... this is necessary because Qt doesn't seem to like adding children which already have children on their own
141     switch (bufferInfo.type()) {
142     case BufferInfo::ChannelBuffer:
143     {
144         ChannelBufferItem *channelBufferItem = static_cast<ChannelBufferItem *>(bufferItem);
145         if (_network) {
146             IrcChannel *ircChannel = _network->ircChannel(bufferInfo.bufferName());
147             if (ircChannel)
148                 channelBufferItem->attachIrcChannel(ircChannel);
149         }
150     }
151     break;
152     default:
153         break;
154     }
155
156     BufferSyncer *bufferSyncer = Client::bufferSyncer();
157     if (bufferSyncer) {
158         bufferItem->addActivity(
159                 bufferSyncer->activity(bufferItem->bufferId()),
160                 bufferSyncer->highlightCount(bufferItem->bufferId()) > 0
161         );
162     }
163
164     return bufferItem;
165 }
166
167
168 void NetworkItem::attachNetwork(Network *network)
169 {
170     if (!network)
171         return;
172
173     _network = network;
174
175     connect(network, SIGNAL(networkNameSet(QString)),
176         this, SLOT(setNetworkName(QString)));
177     connect(network, SIGNAL(currentServerSet(QString)),
178         this, SLOT(setCurrentServer(QString)));
179     connect(network, SIGNAL(ircChannelAdded(IrcChannel *)),
180         this, SLOT(attachIrcChannel(IrcChannel *)));
181     connect(network, SIGNAL(ircUserAdded(IrcUser *)),
182         this, SLOT(attachIrcUser(IrcUser *)));
183     connect(network, SIGNAL(connectedSet(bool)),
184         this, SIGNAL(networkDataChanged()));
185     connect(network, SIGNAL(destroyed()),
186         this, SLOT(onNetworkDestroyed()));
187
188     emit networkDataChanged();
189 }
190
191
192 void NetworkItem::attachIrcChannel(IrcChannel *ircChannel)
193 {
194     ChannelBufferItem *channelItem;
195     for (int i = 0; i < childCount(); i++) {
196         channelItem = qobject_cast<ChannelBufferItem *>(child(i));
197         if (!channelItem)
198             continue;
199
200         if (channelItem->bufferName().toLower() == ircChannel->name().toLower()) {
201             channelItem->attachIrcChannel(ircChannel);
202             return;
203         }
204     }
205 }
206
207
208 void NetworkItem::attachIrcUser(IrcUser *ircUser)
209 {
210     QueryBufferItem *queryItem = 0;
211     for (int i = 0; i < childCount(); i++) {
212         queryItem = qobject_cast<QueryBufferItem *>(child(i));
213         if (!queryItem)
214             continue;
215
216         if (queryItem->bufferName().toLower() == ircUser->nick().toLower()) {
217             queryItem->setIrcUser(ircUser);
218             break;
219         }
220     }
221 }
222
223
224 void NetworkItem::setNetworkName(const QString &networkName)
225 {
226     Q_UNUSED(networkName);
227     emit networkDataChanged(0);
228 }
229
230
231 void NetworkItem::setCurrentServer(const QString &serverName)
232 {
233     Q_UNUSED(serverName);
234     emit networkDataChanged(1);
235 }
236
237
238 QString NetworkItem::toolTip(int column) const
239 {
240     Q_UNUSED(column);
241     QString strTooltip;
242     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
243     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
244
245     // Function to add a row to the tooltip table
246     auto addRow = [&](const QString& key, const QString& value, bool condition) {
247         if (condition) {
248             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
249         }
250     };
251
252     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(networkName(), true) << "</p>";
253     if (isActive()) {
254         tooltip << "<table cellspacing='5' cellpadding='0'>";
255         addRow(tr("Server"), NetworkItem::escapeHTML(currentServer(), true), !currentServer().isEmpty());
256         addRow(tr("Users"), QString::number(nickCount()), true);
257         if (_network)
258             addRow(tr("Lag"), NetworkItem::escapeHTML(tr("%1 msecs").arg(_network->latency()), true), true);
259
260         tooltip << "</table>";
261     } else {
262         tooltip << "<p class='italic' align='center'>" << tr("Not connected") << "</p>";
263     }
264     tooltip << "</qt>";
265     return strTooltip;
266 }
267
268
269 void NetworkItem::onBeginRemoveChilds(int start, int end)
270 {
271     for (int i = start; i <= end; i++) {
272         StatusBufferItem *statusBufferItem = qobject_cast<StatusBufferItem *>(child(i));
273         if (statusBufferItem) {
274             _statusBufferItem = 0;
275             break;
276         }
277     }
278 }
279
280
281 void NetworkItem::onNetworkDestroyed()
282 {
283     _network = 0;
284     emit networkDataChanged();
285     removeAllChilds();
286 }
287
288
289 /*****************************************
290 *  Fancy Buffer Items
291 *****************************************/
292 BufferItem::BufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent)
293     : PropertyMapItem(parent),
294     _bufferInfo(bufferInfo),
295     _activity(BufferInfo::NoActivity)
296 {
297     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
298 }
299
300
301 QStringList BufferItem::propertyOrder() const
302 {
303     static QStringList order{"bufferName", "topic", "nickCount"};
304     return order;
305 }
306
307
308 void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level)
309 {
310     if (_activity != level) {
311         _activity = level;
312         emit dataChanged();
313     }
314 }
315
316
317 void BufferItem::clearActivityLevel()
318 {
319     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
320         // If the core handles activity sync, clear only the highlight flag
321         _activity &= ~BufferInfo::Highlight;
322     } else {
323         _activity = BufferInfo::NoActivity;
324     }
325     _firstUnreadMsgId = MsgId();
326
327     // FIXME remove with core proto v11
328     if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
329         _markerLineMsgId = _lastSeenMsgId;
330     }
331
332     emit dataChanged();
333 }
334
335
336 void BufferItem::updateActivityLevel(const Message &msg)
337 {
338     // If the core handles activity, and this message is not a highlight, ignore this
339     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) {
340         return;
341     }
342
343     if (isCurrentBuffer()) {
344         return;
345     }
346
347     if (msg.flags() & Message::Self)    // don't update activity for our own messages
348         return;
349
350     if (Client::ignoreListManager()
351         && Client::ignoreListManager()->match(msg, qobject_cast<NetworkItem *>(parent())->networkName()))
352         return;
353
354     if (msg.msgId() <= lastSeenMsgId())
355         return;
356
357     bool stateChanged = false;
358     if (!firstUnreadMsgId().isValid() || msg.msgId() < firstUnreadMsgId()) {
359         stateChanged = true;
360         _firstUnreadMsgId = msg.msgId();
361     }
362
363     Message::Types type;
364     // If the core handles activities, ignore types
365     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
366         type = Message::Types();
367     } else {
368         type = msg.type();
369     }
370
371     if (addActivity(type, msg.flags().testFlag(Message::Highlight)) || stateChanged) {
372         emit dataChanged();
373     }
374 }
375
376 void BufferItem::setActivity(Message::Types type, bool highlight) {
377     BufferInfo::ActivityLevel oldLevel = activityLevel();
378
379     _activity &= BufferInfo::Highlight;
380     addActivity(type, highlight);
381
382     if (_activity != oldLevel) {
383         emit dataChanged();
384     }
385 }
386
387 bool BufferItem::addActivity(Message::Types type, bool highlight) {
388     auto oldActivity = activityLevel();
389
390     if (type != Message::Types())
391         _activity |= BufferInfo::OtherActivity;
392
393     if (type.testFlag(Message::Plain) || type.testFlag(Message::Notice) || type.testFlag(Message::Action))
394         _activity |= BufferInfo::NewMessage;
395
396     if (highlight)
397         _activity |= BufferInfo::Highlight;
398
399     return oldActivity != _activity;
400 }
401
402
403 QVariant BufferItem::data(int column, int role) const
404 {
405     switch (role) {
406     case NetworkModel::ItemTypeRole:
407         return NetworkModel::BufferItemType;
408     case NetworkModel::BufferIdRole:
409         return qVariantFromValue(bufferInfo().bufferId());
410     case NetworkModel::NetworkIdRole:
411         return qVariantFromValue(bufferInfo().networkId());
412     case NetworkModel::BufferInfoRole:
413         return qVariantFromValue(bufferInfo());
414     case NetworkModel::BufferTypeRole:
415         return int(bufferType());
416     case NetworkModel::ItemActiveRole:
417         return isActive();
418     case NetworkModel::BufferActivityRole:
419         return (int)activityLevel();
420     case NetworkModel::BufferFirstUnreadMsgIdRole:
421         return qVariantFromValue(firstUnreadMsgId());
422     case NetworkModel::MarkerLineMsgIdRole:
423         return qVariantFromValue(markerLineMsgId());
424     default:
425         return PropertyMapItem::data(column, role);
426     }
427 }
428
429
430 bool BufferItem::setData(int column, const QVariant &value, int role)
431 {
432     switch (role) {
433     case NetworkModel::BufferActivityRole:
434         setActivityLevel((BufferInfo::ActivityLevel)value.toInt());
435         return true;
436     default:
437         return PropertyMapItem::setData(column, value, role);
438     }
439     return true;
440 }
441
442
443 void BufferItem::setBufferName(const QString &name)
444 {
445     _bufferInfo = BufferInfo(_bufferInfo.bufferId(), _bufferInfo.networkId(), _bufferInfo.type(), _bufferInfo.groupId(), name);
446     emit dataChanged(0);
447 }
448
449
450 void BufferItem::setLastSeenMsgId(MsgId msgId)
451 {
452     _lastSeenMsgId = msgId;
453
454     // FIXME remove with core protocol v11
455     if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
456         if (!isCurrentBuffer())
457             _markerLineMsgId = msgId;
458     }
459
460     setActivityLevel(BufferInfo::NoActivity);
461 }
462
463
464 void BufferItem::setMarkerLineMsgId(MsgId msgId)
465 {
466     _markerLineMsgId = msgId;
467     emit dataChanged();
468 }
469
470
471 bool BufferItem::isCurrentBuffer() const
472 {
473     return _bufferInfo.bufferId() == Client::bufferModel()->currentIndex().data(NetworkModel::BufferIdRole).value<BufferId>();
474 }
475
476
477 QString BufferItem::toolTip(int column) const
478 {
479     Q_UNUSED(column);
480     return tr("<p> %1 - %2 </p>").arg(bufferInfo().bufferId().toInt()).arg(bufferName());
481 }
482
483
484 /*****************************************
485 *  StatusBufferItem
486 *****************************************/
487 StatusBufferItem::StatusBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent)
488     : BufferItem(bufferInfo, parent)
489 {
490 }
491
492
493 QString StatusBufferItem::toolTip(int column) const
494 {
495     NetworkItem *networkItem = qobject_cast<NetworkItem *>(parent());
496     if (networkItem)
497         return networkItem->toolTip(column);
498     else
499         return QString();
500 }
501
502
503 /*****************************************
504 *  QueryBufferItem
505 *****************************************/
506 QueryBufferItem::QueryBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent)
507     : BufferItem(bufferInfo, parent),
508     _ircUser(0)
509 {
510     setFlags(flags() | Qt::ItemIsDropEnabled | Qt::ItemIsEditable);
511
512     const Network *net = Client::network(bufferInfo.networkId());
513     if (!net)
514         return;
515
516     IrcUser *ircUser = net->ircUser(bufferInfo.bufferName());
517     setIrcUser(ircUser);
518 }
519
520
521 QVariant QueryBufferItem::data(int column, int role) const
522 {
523     switch (role) {
524     case Qt::EditRole:
525         return BufferItem::data(column, Qt::DisplayRole);
526     case NetworkModel::IrcUserRole:
527         return QVariant::fromValue<QObject *>(_ircUser);
528     case NetworkModel::UserAwayRole:
529         return (bool)_ircUser ? _ircUser->isAway() : false;
530     default:
531         return BufferItem::data(column, role);
532     }
533 }
534
535
536 bool QueryBufferItem::setData(int column, const QVariant &value, int role)
537 {
538     if (column != 0)
539         return BufferItem::setData(column, value, role);
540
541     switch (role) {
542     case Qt::EditRole:
543     {
544         QString newName = value.toString();
545
546         // Sanity check - buffer names must not contain newlines!
547         int nlpos = newName.indexOf('\n');
548         if (nlpos >= 0)
549             newName = newName.left(nlpos);
550
551         if (!newName.isEmpty()) {
552             Client::renameBuffer(bufferId(), newName);
553             return true;
554         }
555         else {
556             return false;
557         }
558     }
559     break;
560     default:
561         return BufferItem::setData(column, value, role);
562     }
563 }
564
565
566 void QueryBufferItem::setBufferName(const QString &name)
567 {
568     BufferItem::setBufferName(name);
569     NetworkId netId = data(0, NetworkModel::NetworkIdRole).value<NetworkId>();
570     const Network *net = Client::network(netId);
571     if (net)
572         setIrcUser(net->ircUser(name));
573 }
574
575
576 QString QueryBufferItem::toolTip(int column) const
577 {
578     // pretty much code duplication of IrcUserItem::toolTip() but inheritance won't solve this...
579     Q_UNUSED(column);
580     QString strTooltip;
581     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
582     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
583
584     // Keep track of whether or not information has been added
585     bool infoAdded = false;
586
587     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
588     tooltip << "<p class='bold' align='center'>";
589     tooltip << tr("Query with %1").arg(NetworkItem::escapeHTML(bufferName(), true));
590     if (!_ircUser) {
591         // User seems to be offline, let the no information message be added below
592         tooltip << "</p>";
593     } else {
594         // Function to add a row to the tooltip table
595         auto addRow = [&](const QString& key, const QString& value, bool condition) {
596             if (condition) {
597                 tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
598                 infoAdded = true;
599             }
600         };
601
602         // User information is available
603         if (_ircUser->userModes() != "") {
604             //TODO Translate user Modes and add them to the table below and in IrcUserItem::toolTip
605             tooltip << " (" << _ircUser->userModes() << ")";
606         }
607         tooltip << "</p>";
608
609         tooltip << "<table cellspacing='5' cellpadding='0'>";
610         if (_ircUser->isAway()) {
611             QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
612
613             // If away message is known, replace with the escaped message.
614             if (!_ircUser->awayMessage().isEmpty()) {
615                 awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
616             }
617             addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
618         }
619         addRow(tr("Realname"),
620                NetworkItem::escapeHTML(_ircUser->realName()),
621                !_ircUser->realName().isEmpty());
622         // suserHost may return "<nick> is available for help", which should be translated.
623         // See https://www.alien.net.au/irc/irc2numerics.html
624         if(_ircUser->suserHost().endsWith("available for help")) {
625             addRow(NetworkItem::escapeHTML(tr("Help status"), true),
626                    NetworkItem::escapeHTML(tr("Available for help")),
627                    true);
628         } else {
629             addRow(NetworkItem::escapeHTML(tr("Service status"), true),
630                    NetworkItem::escapeHTML(_ircUser->suserHost()),
631                    !_ircUser->suserHost().isEmpty());
632         }
633
634         // Keep track of whether or not the account information's been added.  Don't show it twice.
635         bool accountAdded = false;
636         if(!_ircUser->account().isEmpty()) {
637             // IRCv3 account-notify is supported by the core and IRC server.
638             // Assume logged out (seems to be more common)
639             QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
640
641             // If account is logged in, replace with the escaped account name.
642             if (_ircUser->account() != "*") {
643                 accountHTML = NetworkItem::escapeHTML(_ircUser->account());
644             }
645             addRow(NetworkItem::escapeHTML(tr("Account"), true),
646                    accountHTML,
647                    true);
648             // Mark the row as added
649             accountAdded = true;
650         }
651         // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
652         // See https://www.alien.net.au/irc/irc2numerics.html
653         if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
654             addRow(NetworkItem::escapeHTML(tr("Account"), true),
655                    NetworkItem::escapeHTML(tr("Identified for this nick")),
656                    !accountAdded);
657             // Don't add the account row again if information's already added via account-notify
658             // Not used further down...
659             // accountAdded = true;
660         } else {
661             addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
662                    NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
663                    !_ircUser->whoisServiceReply().isEmpty());
664         }
665         addRow(tr("Hostmask"),
666                NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
667                !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
668         // ircOperator may contain "is an" or "is a", which should be removed.
669         addRow(tr("Operator"),
670                NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
671                !_ircUser->ircOperator().isEmpty());
672
673         if (_ircUser->idleTime().isValid()) {
674             QDateTime now = QDateTime::currentDateTime();
675             QDateTime idle = _ircUser->idleTime();
676             int idleTime = idle.secsTo(now);
677             addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
678         }
679
680         if (_ircUser->loginTime().isValid()) {
681             addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
682         }
683
684         addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
685         tooltip << "</table>";
686     }
687
688     // If no further information found, offer an explanatory message
689     if (!infoAdded)
690         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
691
692     tooltip << "</qt>";
693     return strTooltip;
694 }
695
696
697 void QueryBufferItem::setIrcUser(IrcUser *ircUser)
698 {
699     if (_ircUser == ircUser)
700         return;
701
702     if (_ircUser) {
703         disconnect(_ircUser, 0, this, 0);
704     }
705
706     if (ircUser) {
707         connect(ircUser, SIGNAL(destroyed(QObject*)), SLOT(removeIrcUser()));
708         connect(ircUser, SIGNAL(quited()), this, SLOT(removeIrcUser()));
709         connect(ircUser, SIGNAL(awaySet(bool)), this, SIGNAL(dataChanged()));
710         connect(ircUser, SIGNAL(encryptedSet(bool)), this, SLOT(setEncrypted(bool)));
711     }
712
713     _ircUser = ircUser;
714     emit dataChanged();
715 }
716
717
718 void QueryBufferItem::removeIrcUser()
719 {
720     if (_ircUser) {
721         // Disconnect the active IrcUser before removing it, otherwise it will fire removeIrcUser()
722         // a second time when the object's destroyed due to QueryBufferItem::setIrcUser() connecting
723         // SIGNAL destroyed(QObject*) to SLOT removeIrcUser().
724         // This fixes removing an active IrcUser if the user had quit then rejoined in a nonstandard
725         // manner (e.g. updateNickFromHost calling newIrcUser, triggered by an away-notify message).
726         disconnect(_ircUser, 0, this, 0);
727
728         // Clear IrcUser (only set to 0 if not already 0)
729         _ircUser = 0;
730
731         // Only emit dataChanged() if data actually changed.  This might serve as a small
732         // optimization, but it can be moved outside the if statement if other behavior depends on
733         // it always being called.
734         emit dataChanged();
735     }
736 }
737
738
739 /*****************************************
740 *  ChannelBufferItem
741 *****************************************/
742 ChannelBufferItem::ChannelBufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent)
743     : BufferItem(bufferInfo, parent),
744     _ircChannel(0)
745 {
746     setFlags(flags() | Qt::ItemIsDropEnabled);
747 }
748
749
750 QVariant ChannelBufferItem::data(int column, int role) const
751 {
752     switch (role) {
753     case NetworkModel::IrcChannelRole:
754         return QVariant::fromValue<QObject *>(_ircChannel);
755     default:
756         return BufferItem::data(column, role);
757     }
758 }
759
760
761 QString ChannelBufferItem::toolTip(int column) const
762 {
763     Q_UNUSED(column);
764     QString strTooltip;
765     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
766     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
767
768     // Function to add a row to the tooltip table
769     auto addRow = [&](const QString& key, const QString& value, bool condition) {
770         if (condition) {
771             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
772         }
773     };
774
775     tooltip << "<p class='bold' align='center'>";
776     tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "</p>";
777
778     if (isActive()) {
779         tooltip << "<table cellspacing='5' cellpadding='0'>";
780         addRow(tr("Users"), QString::number(nickCount()), true);
781
782         if (_ircChannel) {
783             QString channelMode = _ircChannel->channelModeString(); // channelModeString is compiled on the fly -> thus cache the result
784             if (!channelMode.isEmpty())
785                 addRow(tr("Mode"), channelMode, true);
786         }
787
788         ItemViewSettings s;
789         bool showTopic = s.displayTopicInTooltip();
790         if (showTopic) {
791             QString _topic = topic();
792             if (_topic != "") {
793                 _topic = stripFormatCodes(_topic);
794                 _topic = NetworkItem::escapeHTML(_topic);
795                 addRow(tr("Topic"), _topic, true);
796             }
797         }
798
799         tooltip << "</table>";
800     } else {
801         tooltip << "<p class='italic' align='center'>" << tr("Not active, double-click to join") << "</p>";
802     }
803
804     tooltip << "</qt>";
805     return strTooltip;
806 }
807
808
809 void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel)
810 {
811     if (_ircChannel) {
812         qWarning() << Q_FUNC_INFO << "IrcChannel already set; cleanup failed!?";
813         disconnect(_ircChannel, 0, this, 0);
814     }
815
816     _ircChannel = ircChannel;
817
818     connect(ircChannel, SIGNAL(destroyed(QObject*)),
819         this, SLOT(ircChannelDestroyed()));
820     connect(ircChannel, SIGNAL(topicSet(QString)),
821         this, SLOT(setTopic(QString)));
822     connect(ircChannel, SIGNAL(encryptedSet(bool)),
823         this, SLOT(setEncrypted(bool)));
824     connect(ircChannel, SIGNAL(ircUsersJoined(QList<IrcUser *> )),
825         this, SLOT(join(QList<IrcUser *> )));
826     connect(ircChannel, SIGNAL(ircUserParted(IrcUser *)),
827         this, SLOT(part(IrcUser *)));
828     connect(ircChannel, SIGNAL(parted()),
829         this, SLOT(ircChannelParted()));
830     connect(ircChannel, SIGNAL(ircUserModesSet(IrcUser *, QString)),
831         this, SLOT(userModeChanged(IrcUser *)));
832     connect(ircChannel, SIGNAL(ircUserModeAdded(IrcUser *, QString)),
833         this, SLOT(userModeChanged(IrcUser *)));
834     connect(ircChannel, SIGNAL(ircUserModeRemoved(IrcUser *, QString)),
835         this, SLOT(userModeChanged(IrcUser *)));
836
837     if (!ircChannel->ircUsers().isEmpty())
838         join(ircChannel->ircUsers());
839
840     emit dataChanged();
841 }
842
843 QString ChannelBufferItem::nickChannelModes(const QString &nick) const
844 {
845     if (!_ircChannel) {
846         qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes";
847         return QString();
848     }
849
850     return _ircChannel->userModes(nick);
851 }
852
853
854 void ChannelBufferItem::ircChannelParted()
855 {
856     Q_CHECK_PTR(_ircChannel);
857     disconnect(_ircChannel, 0, this, 0);
858     _ircChannel = 0;
859     emit dataChanged();
860     removeAllChilds();
861 }
862
863
864 void ChannelBufferItem::ircChannelDestroyed()
865 {
866     if (_ircChannel) {
867         _ircChannel = 0;
868         emit dataChanged();
869         removeAllChilds();
870     }
871 }
872
873
874 void ChannelBufferItem::join(const QList<IrcUser *> &ircUsers)
875 {
876     addUsersToCategory(ircUsers);
877     emit dataChanged(2);
878 }
879
880
881 UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId)
882 {
883     UserCategoryItem *categoryItem = 0;
884
885     for (int i = 0; i < childCount(); i++) {
886         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
887         if (!categoryItem)
888             continue;
889         if (categoryItem->categoryId() == categoryId)
890             return categoryItem;
891     }
892     return 0;
893 }
894
895
896 void ChannelBufferItem::addUserToCategory(IrcUser *ircUser)
897 {
898     addUsersToCategory(QList<IrcUser *>() << ircUser);
899 }
900
901
902 void ChannelBufferItem::addUsersToCategory(const QList<IrcUser *> &ircUsers)
903 {
904     Q_ASSERT(_ircChannel);
905
906     QHash<UserCategoryItem *, QList<IrcUser *> > categories;
907
908     int categoryId = -1;
909     UserCategoryItem *categoryItem = 0;
910
911     foreach(IrcUser *ircUser, ircUsers) {
912         categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
913         categoryItem = findCategoryItem(categoryId);
914         if (!categoryItem) {
915             categoryItem = new UserCategoryItem(categoryId, this);
916             categories[categoryItem] = QList<IrcUser *>();
917             newChild(categoryItem);
918         }
919         categories[categoryItem] << ircUser;
920     }
921
922     QHash<UserCategoryItem *, QList<IrcUser *> >::const_iterator catIter = categories.constBegin();
923     while (catIter != categories.constEnd()) {
924         catIter.key()->addUsers(catIter.value());
925         ++catIter;
926     }
927 }
928
929
930 void ChannelBufferItem::part(IrcUser *ircUser)
931 {
932     if (!ircUser) {
933         qWarning() << bufferName() << "ChannelBufferItem::part(): unknown User" << ircUser;
934         return;
935     }
936
937     disconnect(ircUser, 0, this, 0);
938     removeUserFromCategory(ircUser);
939     emit dataChanged(2);
940 }
941
942
943 void ChannelBufferItem::removeUserFromCategory(IrcUser *ircUser)
944 {
945     if (!_ircChannel) {
946         // If we parted the channel there might still be some ircUsers connected.
947         // in that case we just ignore the call
948         Q_ASSERT(childCount() == 0);
949         return;
950     }
951
952     UserCategoryItem *categoryItem = 0;
953     for (int i = 0; i < childCount(); i++) {
954         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
955         if (categoryItem->removeUser(ircUser)) {
956             if (categoryItem->childCount() == 0)
957                 removeChild(i);
958             break;
959         }
960     }
961 }
962
963
964 void ChannelBufferItem::userModeChanged(IrcUser *ircUser)
965 {
966     Q_ASSERT(_ircChannel);
967
968     int categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
969     UserCategoryItem *categoryItem = findCategoryItem(categoryId);
970
971     if (categoryItem) {
972         if (categoryItem->findIrcUser(ircUser)) {
973             return; // already in the right category;
974         }
975     }
976     else {
977         categoryItem = new UserCategoryItem(categoryId, this);
978         newChild(categoryItem);
979     }
980
981     // find the item that needs reparenting
982     IrcUserItem *ircUserItem = 0;
983     for (int i = 0; i < childCount(); i++) {
984         UserCategoryItem *oldCategoryItem = qobject_cast<UserCategoryItem *>(child(i));
985         Q_ASSERT(oldCategoryItem);
986         IrcUserItem *userItem = oldCategoryItem->findIrcUser(ircUser);
987         if (userItem) {
988             ircUserItem = userItem;
989             break;
990         }
991     }
992
993     if (!ircUserItem) {
994         qWarning() << "ChannelBufferItem::userModeChanged(IrcUser *): unable to determine old category of" << ircUser;
995         return;
996     }
997     ircUserItem->reParent(categoryItem);
998 }
999
1000
1001 /*****************************************
1002 *  User Category Items (like @vh etc.)
1003 *****************************************/
1004 // we hardcode this even though we have PREFIX in network... but that wouldn't help with mapping modes to
1005 // category strings anyway.
1006 const QList<QChar> UserCategoryItem::categories = QList<QChar>() << 'q' << 'a' << 'o' << 'h' << 'v';
1007
1008 UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent)
1009     : PropertyMapItem(parent),
1010     _category(category)
1011 {
1012     setFlags(Qt::ItemIsEnabled);
1013     setTreeItemFlags(AbstractTreeItem::DeleteOnLastChildRemoved);
1014     setObjectName(parent->data(0, Qt::DisplayRole).toString() + "/" + QString::number(category));
1015 }
1016
1017
1018 QStringList UserCategoryItem::propertyOrder() const
1019 {
1020     static QStringList order{"categoryName"};
1021     return order;
1022 }
1023
1024
1025 // caching this makes no sense, since we display the user number dynamically
1026 QString UserCategoryItem::categoryName() const
1027 {
1028     int n = childCount();
1029     switch (_category) {
1030     case 0:
1031         return tr("%n Owner(s)", 0, n);
1032     case 1:
1033         return tr("%n Admin(s)", 0, n);
1034     case 2:
1035         return tr("%n Operator(s)", 0, n);
1036     case 3:
1037         return tr("%n Half-Op(s)", 0, n);
1038     case 4:
1039         return tr("%n Voiced", 0, n);
1040     default:
1041         return tr("%n User(s)", 0, n);
1042     }
1043 }
1044
1045
1046 IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser)
1047 {
1048     IrcUserItem *userItem = 0;
1049
1050     for (int i = 0; i < childCount(); i++) {
1051         userItem = qobject_cast<IrcUserItem *>(child(i));
1052         if (!userItem)
1053             continue;
1054         if (userItem->ircUser() == ircUser)
1055             return userItem;
1056     }
1057     return 0;
1058 }
1059
1060
1061 void UserCategoryItem::addUsers(const QList<IrcUser *> &ircUsers)
1062 {
1063     QList<AbstractTreeItem *> userItems;
1064     foreach(IrcUser *ircUser, ircUsers)
1065     userItems << new IrcUserItem(ircUser, this);
1066     newChilds(userItems);
1067     emit dataChanged(0);
1068 }
1069
1070
1071 bool UserCategoryItem::removeUser(IrcUser *ircUser)
1072 {
1073     IrcUserItem *userItem = findIrcUser(ircUser);
1074     bool success = (bool)userItem;
1075     if (success) {
1076         removeChild(userItem);
1077         emit dataChanged(0);
1078     }
1079     return success;
1080 }
1081
1082
1083 int UserCategoryItem::categoryFromModes(const QString &modes)
1084 {
1085     for (int i = 0; i < categories.count(); i++) {
1086         if (modes.contains(categories[i]))
1087             return i;
1088     }
1089     return categories.count();
1090 }
1091
1092
1093 QVariant UserCategoryItem::data(int column, int role) const
1094 {
1095     switch (role) {
1096     case TreeModel::SortRole:
1097         return _category;
1098     case NetworkModel::ItemActiveRole:
1099         return true;
1100     case NetworkModel::ItemTypeRole:
1101         return NetworkModel::UserCategoryItemType;
1102     case NetworkModel::BufferIdRole:
1103         return parent()->data(column, role);
1104     case NetworkModel::NetworkIdRole:
1105         return parent()->data(column, role);
1106     case NetworkModel::BufferInfoRole:
1107         return parent()->data(column, role);
1108     default:
1109         return PropertyMapItem::data(column, role);
1110     }
1111 }
1112
1113
1114 /*****************************************
1115 *  Irc User Items
1116 *****************************************/
1117 IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent)
1118     : PropertyMapItem(parent),
1119     _ircUser(ircUser)
1120 {
1121     setObjectName(ircUser->nick());
1122     connect(ircUser, SIGNAL(quited()), this, SLOT(ircUserQuited()));
1123     connect(ircUser, SIGNAL(nickSet(QString)), this, SIGNAL(dataChanged()));
1124     connect(ircUser, SIGNAL(awaySet(bool)), this, SIGNAL(dataChanged()));
1125 }
1126
1127
1128 QStringList IrcUserItem::propertyOrder() const
1129 {
1130     static QStringList order{"nickName"};
1131     return order;
1132 }
1133
1134
1135 QVariant IrcUserItem::data(int column, int role) const
1136 {
1137     switch (role) {
1138     case NetworkModel::ItemActiveRole:
1139         return isActive();
1140     case NetworkModel::ItemTypeRole:
1141         return NetworkModel::IrcUserItemType;
1142     case NetworkModel::BufferIdRole:
1143         return parent()->data(column, role);
1144     case NetworkModel::NetworkIdRole:
1145         return parent()->data(column, role);
1146     case NetworkModel::BufferInfoRole:
1147         return parent()->data(column, role);
1148     case NetworkModel::IrcChannelRole:
1149         return parent()->data(column, role);
1150     case NetworkModel::IrcUserRole:
1151         return QVariant::fromValue<QObject *>(_ircUser.data());
1152     case NetworkModel::UserAwayRole:
1153         return (bool)_ircUser ? _ircUser->isAway() : false;
1154     default:
1155         return PropertyMapItem::data(column, role);
1156     }
1157 }
1158
1159
1160 QString IrcUserItem::toolTip(int column) const
1161 {
1162     Q_UNUSED(column);
1163     QString strTooltip;
1164     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
1165     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
1166
1167     // Keep track of whether or not information has been added
1168     bool infoAdded = false;
1169
1170     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
1171     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(nickName(), true);
1172     if (_ircUser->userModes() != "") {
1173         //TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip
1174         tooltip << " (" << _ircUser->userModes() << ")";
1175     }
1176     tooltip << "</p>";
1177
1178     auto addRow = [&](const QString& key, const QString& value, bool condition) {
1179         if (condition)
1180         {
1181             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
1182             infoAdded = true;
1183         }
1184     };
1185
1186     tooltip << "<table cellspacing='5' cellpadding='0'>";
1187     addRow(tr("Modes"),
1188            NetworkItem::escapeHTML(channelModes()),
1189            !channelModes().isEmpty());
1190     if (_ircUser->isAway()) {
1191         QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
1192
1193         // If away message is known, replace with the escaped message.
1194         if (!_ircUser->awayMessage().isEmpty()) {
1195             awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
1196         }
1197         addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
1198     }
1199     addRow(tr("Realname"),
1200            NetworkItem::escapeHTML(_ircUser->realName()),
1201            !_ircUser->realName().isEmpty());
1202
1203     // suserHost may return "<nick> is available for help", which should be translated.
1204     // See https://www.alien.net.au/irc/irc2numerics.html
1205     if(_ircUser->suserHost().endsWith("available for help")) {
1206         addRow(NetworkItem::escapeHTML(tr("Help status"), true),
1207                NetworkItem::escapeHTML(tr("Available for help")),
1208                true);
1209     } else {
1210         addRow(NetworkItem::escapeHTML(tr("Service status"), true),
1211                NetworkItem::escapeHTML(_ircUser->suserHost()),
1212                !_ircUser->suserHost().isEmpty());
1213     }
1214
1215     // Keep track of whether or not the account information's been added.  Don't show it twice.
1216     bool accountAdded = false;
1217     if(!_ircUser->account().isEmpty()) {
1218         // IRCv3 account-notify is supported by the core and IRC server.
1219         // Assume logged out (seems to be more common)
1220         QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
1221
1222         // If account is logged in, replace with the escaped account name.
1223         if (_ircUser->account() != "*") {
1224             accountHTML = NetworkItem::escapeHTML(_ircUser->account());
1225         }
1226         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1227                accountHTML,
1228                true);
1229         // Mark the row as added
1230         accountAdded = true;
1231     }
1232     // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
1233     // See https://www.alien.net.au/irc/irc2numerics.html
1234     if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
1235         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1236                NetworkItem::escapeHTML(tr("Identified for this nick")),
1237                !accountAdded);
1238         // Don't add the account row again if information's already added via account-notify
1239         // Not used further down...
1240         // accountAdded = true;
1241     } else {
1242         addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
1243                NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
1244                !_ircUser->whoisServiceReply().isEmpty());
1245     }
1246     addRow(tr("Hostmask"),
1247            NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
1248            !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
1249     // ircOperator may contain "is an" or "is a", which should be removed.
1250     addRow(tr("Operator"),
1251            NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
1252            !_ircUser->ircOperator().isEmpty());
1253
1254     if (_ircUser->idleTime().isValid()) {
1255         QDateTime now = QDateTime::currentDateTime();
1256         QDateTime idle = _ircUser->idleTime();
1257         int idleTime = idle.secsTo(now);
1258         addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
1259     }
1260
1261     if (_ircUser->loginTime().isValid()) {
1262         addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
1263     }
1264
1265     addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
1266     tooltip << "</table>";
1267
1268     // If no further information found, offer an explanatory message
1269     if (!infoAdded)
1270         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
1271
1272     tooltip << "</qt>";
1273     return strTooltip;
1274 }
1275
1276 QString IrcUserItem::channelModes() const
1277 {
1278     // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem.
1279     // We want the channel buffer item in order to get the channel-specific user modes.
1280     UserCategoryItem *category = qobject_cast<UserCategoryItem *>(parent());
1281     if (!category)
1282         return QString();
1283
1284     ChannelBufferItem *channel = qobject_cast<ChannelBufferItem *>(category->parent());
1285     if (!channel)
1286         return QString();
1287
1288     return channel->nickChannelModes(nickName());
1289 }
1290
1291
1292 /*****************************************
1293  * NetworkModel
1294  *****************************************/
1295 NetworkModel::NetworkModel(QObject *parent)
1296     : TreeModel(NetworkModel::defaultHeader(), parent)
1297 {
1298     connect(this, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
1299         this, SLOT(checkForNewBuffers(const QModelIndex &, int, int)));
1300     connect(this, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
1301         this, SLOT(checkForRemovedBuffers(const QModelIndex &, int, int)));
1302
1303     BufferSettings defaultSettings;
1304     defaultSettings.notify("UserNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1305     defaultSettings.notify("ServerNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1306     defaultSettings.notify("ErrorMsgsTarget", this, SLOT(messageRedirectionSettingsChanged()));
1307     messageRedirectionSettingsChanged();
1308 }
1309
1310
1311 QList<QVariant> NetworkModel::defaultHeader()
1312 {
1313     QList<QVariant> data;
1314     data << tr("Chat") << tr("Topic") << tr("Nick Count");
1315     return data;
1316 }
1317
1318
1319 bool NetworkModel::isBufferIndex(const QModelIndex &index) const
1320 {
1321     return index.data(NetworkModel::ItemTypeRole) == NetworkModel::BufferItemType;
1322 }
1323
1324
1325 int NetworkModel::networkRow(NetworkId networkId) const
1326 {
1327     NetworkItem *netItem = 0;
1328     for (int i = 0; i < rootItem->childCount(); i++) {
1329         netItem = qobject_cast<NetworkItem *>(rootItem->child(i));
1330         if (!netItem)
1331             continue;
1332         if (netItem->networkId() == networkId)
1333             return i;
1334     }
1335     return -1;
1336 }
1337
1338
1339 QModelIndex NetworkModel::networkIndex(NetworkId networkId)
1340 {
1341     int netRow = networkRow(networkId);
1342     if (netRow == -1)
1343         return QModelIndex();
1344     else
1345         return indexByItem(qobject_cast<NetworkItem *>(rootItem->child(netRow)));
1346 }
1347
1348
1349 NetworkItem *NetworkModel::findNetworkItem(NetworkId networkId) const
1350 {
1351     int netRow = networkRow(networkId);
1352     if (netRow == -1)
1353         return 0;
1354     else
1355         return qobject_cast<NetworkItem *>(rootItem->child(netRow));
1356 }
1357
1358
1359 NetworkItem *NetworkModel::networkItem(NetworkId networkId)
1360 {
1361     NetworkItem *netItem = findNetworkItem(networkId);
1362
1363     if (netItem == 0) {
1364         netItem = new NetworkItem(networkId, rootItem);
1365         rootItem->newChild(netItem);
1366     }
1367     return netItem;
1368 }
1369
1370
1371 void NetworkModel::networkRemoved(const NetworkId &networkId)
1372 {
1373     int netRow = networkRow(networkId);
1374     if (netRow != -1) {
1375         rootItem->removeChild(netRow);
1376     }
1377 }
1378
1379
1380 QModelIndex NetworkModel::bufferIndex(BufferId bufferId)
1381 {
1382     if (!_bufferItemCache.contains(bufferId))
1383         return QModelIndex();
1384
1385     return indexByItem(_bufferItemCache[bufferId]);
1386 }
1387
1388
1389 BufferItem *NetworkModel::findBufferItem(BufferId bufferId) const
1390 {
1391     if (_bufferItemCache.contains(bufferId))
1392         return _bufferItemCache[bufferId];
1393     else
1394         return 0;
1395 }
1396
1397
1398 BufferItem *NetworkModel::bufferItem(const BufferInfo &bufferInfo)
1399 {
1400     if (_bufferItemCache.contains(bufferInfo.bufferId()))
1401         return _bufferItemCache[bufferInfo.bufferId()];
1402
1403     NetworkItem *netItem = networkItem(bufferInfo.networkId());
1404     return netItem->bufferItem(bufferInfo);
1405 }
1406
1407
1408 QStringList NetworkModel::mimeTypes() const
1409 {
1410     // mimetypes we accept for drops
1411     QStringList types;
1412     // comma separated list of colon separated pairs of networkid and bufferid
1413     // example: 0:1,0:2,1:4
1414     types << "application/Quassel/BufferItemList";
1415     return types;
1416 }
1417
1418
1419 bool NetworkModel::mimeContainsBufferList(const QMimeData *mimeData)
1420 {
1421     return mimeData->hasFormat("application/Quassel/BufferItemList");
1422 }
1423
1424
1425 QList<QPair<NetworkId, BufferId> > NetworkModel::mimeDataToBufferList(const QMimeData *mimeData)
1426 {
1427     QList<QPair<NetworkId, BufferId> > bufferList;
1428
1429     if (!mimeContainsBufferList(mimeData))
1430         return bufferList;
1431
1432     QStringList rawBufferList = QString::fromLatin1(mimeData->data("application/Quassel/BufferItemList")).split(",");
1433     NetworkId networkId;
1434     BufferId bufferUid;
1435     foreach(QString rawBuffer, rawBufferList) {
1436         if (!rawBuffer.contains(":"))
1437             continue;
1438         networkId = rawBuffer.section(":", 0, 0).toInt();
1439         bufferUid = rawBuffer.section(":", 1, 1).toInt();
1440         bufferList.append(qMakePair(networkId, bufferUid));
1441     }
1442     return bufferList;
1443 }
1444
1445
1446 QMimeData *NetworkModel::mimeData(const QModelIndexList &indexes) const
1447 {
1448     QMimeData *mimeData = new QMimeData();
1449
1450     QStringList bufferlist;
1451     QString netid, uid, bufferid;
1452     foreach(QModelIndex index, indexes) {
1453         netid = QString::number(index.data(NetworkIdRole).value<NetworkId>().toInt());
1454         uid = QString::number(index.data(BufferIdRole).value<BufferId>().toInt());
1455         bufferid = QString("%1:%2").arg(netid).arg(uid);
1456         if (!bufferlist.contains(bufferid))
1457             bufferlist << bufferid;
1458     }
1459
1460     mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toLatin1());
1461
1462     return mimeData;
1463 }
1464
1465
1466 void NetworkModel::attachNetwork(Network *net)
1467 {
1468     NetworkItem *netItem = networkItem(net->networkId());
1469     netItem->attachNetwork(net);
1470 }
1471
1472
1473 void NetworkModel::bufferUpdated(BufferInfo bufferInfo)
1474 {
1475     BufferItem *bufItem = bufferItem(bufferInfo);
1476     QModelIndex itemindex = indexByItem(bufItem);
1477     emit dataChanged(itemindex, itemindex);
1478 }
1479
1480
1481 void NetworkModel::removeBuffer(BufferId bufferId)
1482 {
1483     BufferItem *buffItem = findBufferItem(bufferId);
1484     if (!buffItem)
1485         return;
1486
1487     buffItem->parent()->removeChild(buffItem);
1488 }
1489
1490
1491 MsgId NetworkModel::lastSeenMsgId(BufferId bufferId) const
1492 {
1493     if (!_bufferItemCache.contains(bufferId))
1494         return MsgId();
1495
1496     return _bufferItemCache[bufferId]->lastSeenMsgId();
1497 }
1498
1499
1500 MsgId NetworkModel::markerLineMsgId(BufferId bufferId) const
1501 {
1502     if (!_bufferItemCache.contains(bufferId))
1503         return MsgId();
1504
1505     return _bufferItemCache[bufferId]->markerLineMsgId();
1506 }
1507
1508
1509 // FIXME we always seem to use this (expensive) non-const version
1510 MsgId NetworkModel::lastSeenMsgId(const BufferId &bufferId)
1511 {
1512     BufferItem *bufferItem = findBufferItem(bufferId);
1513     if (!bufferItem) {
1514         qDebug() << "NetworkModel::lastSeenMsgId(): buffer is unknown:" << bufferId;
1515         Client::purgeKnownBufferIds();
1516         return MsgId();
1517     }
1518     return bufferItem->lastSeenMsgId();
1519 }
1520
1521
1522 void NetworkModel::setLastSeenMsgId(const BufferId &bufferId, const MsgId &msgId)
1523 {
1524     BufferItem *bufferItem = findBufferItem(bufferId);
1525     if (!bufferItem) {
1526         qDebug() << "NetworkModel::setLastSeenMsgId(): buffer is unknown:" << bufferId;
1527         Client::purgeKnownBufferIds();
1528         return;
1529     }
1530     bufferItem->setLastSeenMsgId(msgId);
1531     emit lastSeenMsgSet(bufferId, msgId);
1532 }
1533
1534
1535 void NetworkModel::setMarkerLineMsgId(const BufferId &bufferId, const MsgId &msgId)
1536 {
1537     BufferItem *bufferItem = findBufferItem(bufferId);
1538     if (!bufferItem) {
1539         qDebug() << "NetworkModel::setMarkerLineMsgId(): buffer is unknown:" << bufferId;
1540         Client::purgeKnownBufferIds();
1541         return;
1542     }
1543     bufferItem->setMarkerLineMsgId(msgId);
1544     emit markerLineSet(bufferId, msgId);
1545 }
1546
1547
1548 void NetworkModel::updateBufferActivity(Message &msg)
1549 {
1550     int redirectionTarget = 0;
1551     switch (msg.type()) {
1552     case Message::Notice:
1553         if (bufferType(msg.bufferId()) != BufferInfo::ChannelBuffer) {
1554             msg.setFlags(msg.flags() | Message::Redirected);
1555             if (msg.flags() & Message::ServerMsg) {
1556                 // server notice
1557                 redirectionTarget = _serverNoticesTarget;
1558             }
1559             else {
1560                 redirectionTarget = _userNoticesTarget;
1561             }
1562         }
1563         break;
1564     case Message::Error:
1565         msg.setFlags(msg.flags() | Message::Redirected);
1566         redirectionTarget = _errorMsgsTarget;
1567         break;
1568     // Update IrcUser's last activity
1569     case Message::Plain:
1570     case Message::Action:
1571         if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) {
1572             const Network *net = Client::network(msg.bufferInfo().networkId());
1573             IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : 0;
1574             if (user)
1575                 user->setLastChannelActivity(msg.bufferId(), msg.timestamp());
1576         }
1577         break;
1578     default:
1579         break;
1580     }
1581
1582     if (msg.flags() & Message::Redirected) {
1583         if (redirectionTarget & BufferSettings::DefaultBuffer)
1584             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1585
1586         if (redirectionTarget & BufferSettings::StatusBuffer) {
1587             const NetworkItem *netItem = findNetworkItem(msg.bufferInfo().networkId());
1588             if (netItem) {
1589                 updateBufferActivity(netItem->statusBufferItem(), msg);
1590             }
1591         }
1592     }
1593     else {
1594         if ((BufferSettings(msg.bufferId()).messageFilter() & msg.type()) != msg.type())
1595             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1596     }
1597 }
1598
1599
1600 void NetworkModel::updateBufferActivity(BufferItem *bufferItem, const Message &msg)
1601 {
1602     if (!bufferItem)
1603         return;
1604
1605     bufferItem->updateActivityLevel(msg);
1606     if (bufferItem->isCurrentBuffer())
1607         emit requestSetLastSeenMsg(bufferItem->bufferId(), msg.msgId());
1608 }
1609
1610
1611 void NetworkModel::setBufferActivity(const BufferId &bufferId, BufferInfo::ActivityLevel level)
1612 {
1613     BufferItem *bufferItem = findBufferItem(bufferId);
1614     if (!bufferItem) {
1615         qDebug() << "NetworkModel::setBufferActivity(): buffer is unknown:" << bufferId;
1616         return;
1617     }
1618     bufferItem->setActivityLevel(level);
1619 }
1620
1621
1622 void NetworkModel::clearBufferActivity(const BufferId &bufferId)
1623 {
1624     BufferItem *bufferItem = findBufferItem(bufferId);
1625     if (!bufferItem) {
1626         qDebug() << "NetworkModel::clearBufferActivity(): buffer is unknown:" << bufferId;
1627         return;
1628     }
1629     bufferItem->clearActivityLevel();
1630 }
1631
1632
1633 const Network *NetworkModel::networkByIndex(const QModelIndex &index) const
1634 {
1635     QVariant netVariant = index.data(NetworkIdRole);
1636     if (!netVariant.isValid())
1637         return 0;
1638
1639     NetworkId networkId = netVariant.value<NetworkId>();
1640     return Client::network(networkId);
1641 }
1642
1643
1644 void NetworkModel::checkForRemovedBuffers(const QModelIndex &parent, int start, int end)
1645 {
1646     if (parent.data(ItemTypeRole) != NetworkItemType)
1647         return;
1648
1649     for (int row = start; row <= end; row++) {
1650         _bufferItemCache.remove(parent.child(row, 0).data(BufferIdRole).value<BufferId>());
1651     }
1652 }
1653
1654
1655 void NetworkModel::checkForNewBuffers(const QModelIndex &parent, int start, int end)
1656 {
1657     if (parent.data(ItemTypeRole) != NetworkItemType)
1658         return;
1659
1660     for (int row = start; row <= end; row++) {
1661         QModelIndex child = parent.child(row, 0);
1662         _bufferItemCache[child.data(BufferIdRole).value < BufferId > ()] = static_cast<BufferItem *>(child.internalPointer());
1663     }
1664 }
1665
1666
1667 QString NetworkModel::bufferName(BufferId bufferId) const
1668 {
1669     if (!_bufferItemCache.contains(bufferId))
1670         return QString();
1671
1672     return _bufferItemCache[bufferId]->bufferName();
1673 }
1674
1675
1676 BufferInfo::Type NetworkModel::bufferType(BufferId bufferId) const
1677 {
1678     if (!_bufferItemCache.contains(bufferId))
1679         return BufferInfo::InvalidBuffer;
1680
1681     return _bufferItemCache[bufferId]->bufferType();
1682 }
1683
1684
1685 BufferInfo NetworkModel::bufferInfo(BufferId bufferId) const
1686 {
1687     if (!_bufferItemCache.contains(bufferId))
1688         return BufferInfo();
1689
1690     return _bufferItemCache[bufferId]->bufferInfo();
1691 }
1692
1693
1694 NetworkId NetworkModel::networkId(BufferId bufferId) const
1695 {
1696     if (!_bufferItemCache.contains(bufferId))
1697         return NetworkId();
1698
1699     NetworkItem *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1700     if (netItem)
1701         return netItem->networkId();
1702     else
1703         return NetworkId();
1704 }
1705
1706
1707 QString NetworkModel::networkName(BufferId bufferId) const
1708 {
1709     if (!_bufferItemCache.contains(bufferId))
1710         return QString();
1711
1712     NetworkItem *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1713     if (netItem)
1714         return netItem->networkName();
1715     else
1716         return QString();
1717 }
1718
1719
1720 BufferId NetworkModel::bufferId(NetworkId networkId, const QString &bufferName, Qt::CaseSensitivity cs) const
1721 {
1722     const NetworkItem *netItem = findNetworkItem(networkId);
1723     if (!netItem)
1724         return BufferId();
1725
1726     for (int i = 0; i < netItem->childCount(); i++) {
1727         BufferItem *bufferItem = qobject_cast<BufferItem *>(netItem->child(i));
1728         if (bufferItem && !bufferItem->bufferName().compare(bufferName, cs))
1729             return bufferItem->bufferId();
1730     }
1731     return BufferId();
1732 }
1733
1734
1735 void NetworkModel::sortBufferIds(QList<BufferId> &bufferIds) const
1736 {
1737     QList<BufferItem *> bufferItems;
1738     foreach(BufferId bufferId, bufferIds) {
1739         if (_bufferItemCache.contains(bufferId))
1740             bufferItems << _bufferItemCache[bufferId];
1741     }
1742
1743     qSort(bufferItems.begin(), bufferItems.end(), bufferItemLessThan);
1744
1745     bufferIds.clear();
1746     foreach(BufferItem *bufferItem, bufferItems) {
1747         bufferIds << bufferItem->bufferId();
1748     }
1749 }
1750
1751
1752 QList<BufferId> NetworkModel::allBufferIdsSorted() const
1753 {
1754     QList<BufferId> bufferIds = allBufferIds();
1755     sortBufferIds(bufferIds);
1756     return bufferIds;
1757 }
1758
1759
1760 bool NetworkModel::bufferItemLessThan(const BufferItem *left, const BufferItem *right)
1761 {
1762     int leftType = left->bufferType();
1763     int rightType = right->bufferType();
1764
1765     if (leftType != rightType)
1766         return leftType < rightType;
1767     else
1768         return QString::compare(left->bufferName(), right->bufferName(), Qt::CaseInsensitive) < 0;
1769 }
1770
1771
1772 void NetworkModel::messageRedirectionSettingsChanged()
1773 {
1774     BufferSettings bufferSettings;
1775
1776     _userNoticesTarget = bufferSettings.userNoticesTarget();
1777     _serverNoticesTarget = bufferSettings.serverNoticesTarget();
1778     _errorMsgsTarget = bufferSettings.errorMsgsTarget();
1779 }
1780
1781 void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity) {
1782     auto _bufferItem = findBufferItem(bufferId);
1783     if (!_bufferItem) {
1784         qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId;
1785         return;
1786     }
1787     auto hiddenTypes = BufferSettings(bufferId).messageFilter();
1788     auto visibleTypes = ~hiddenTypes;
1789     auto activityVisibleTypesIntersection = activity & visibleTypes;
1790     _bufferItem->setActivity(activityVisibleTypesIntersection, false);
1791 }
1792
1793 void NetworkModel::highlightCountChanged(BufferId bufferId, int count) {
1794     auto _bufferItem = findBufferItem(bufferId);
1795     if (!_bufferItem) {
1796         qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId;
1797         return;
1798     }
1799     _bufferItem->addActivity(Message::Types{}, count > 0);
1800 }