qa: Remove dead code
[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 }
440
441
442 void BufferItem::setBufferName(const QString &name)
443 {
444     _bufferInfo = BufferInfo(_bufferInfo.bufferId(), _bufferInfo.networkId(), _bufferInfo.type(), _bufferInfo.groupId(), name);
445     emit dataChanged(0);
446 }
447
448
449 void BufferItem::setLastSeenMsgId(MsgId msgId)
450 {
451     _lastSeenMsgId = msgId;
452
453     // FIXME remove with core protocol v11
454     if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
455         if (!isCurrentBuffer())
456             _markerLineMsgId = msgId;
457     }
458
459     setActivityLevel(BufferInfo::NoActivity);
460 }
461
462
463 void BufferItem::setMarkerLineMsgId(MsgId msgId)
464 {
465     _markerLineMsgId = msgId;
466     emit dataChanged();
467 }
468
469
470 bool BufferItem::isCurrentBuffer() const
471 {
472     return _bufferInfo.bufferId() == Client::bufferModel()->currentIndex().data(NetworkModel::BufferIdRole).value<BufferId>();
473 }
474
475
476 QString BufferItem::toolTip(int column) const
477 {
478     Q_UNUSED(column);
479     return tr("<p> %1 - %2 </p>").arg(bufferInfo().bufferId().toInt()).arg(bufferName());
480 }
481
482
483 /*****************************************
484 *  StatusBufferItem
485 *****************************************/
486 StatusBufferItem::StatusBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent)
487     : BufferItem(bufferInfo, parent)
488 {
489 }
490
491
492 QString StatusBufferItem::toolTip(int column) const
493 {
494     NetworkItem *networkItem = qobject_cast<NetworkItem *>(parent());
495     if (networkItem)
496         return networkItem->toolTip(column);
497     else
498         return QString();
499 }
500
501
502 /*****************************************
503 *  QueryBufferItem
504 *****************************************/
505 QueryBufferItem::QueryBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent)
506     : BufferItem(bufferInfo, parent),
507     _ircUser(0)
508 {
509     setFlags(flags() | Qt::ItemIsDropEnabled | Qt::ItemIsEditable);
510
511     const Network *net = Client::network(bufferInfo.networkId());
512     if (!net)
513         return;
514
515     IrcUser *ircUser = net->ircUser(bufferInfo.bufferName());
516     setIrcUser(ircUser);
517 }
518
519
520 QVariant QueryBufferItem::data(int column, int role) const
521 {
522     switch (role) {
523     case Qt::EditRole:
524         return BufferItem::data(column, Qt::DisplayRole);
525     case NetworkModel::IrcUserRole:
526         return QVariant::fromValue<QObject *>(_ircUser);
527     case NetworkModel::UserAwayRole:
528         return (bool)_ircUser ? _ircUser->isAway() : false;
529     default:
530         return BufferItem::data(column, role);
531     }
532 }
533
534
535 bool QueryBufferItem::setData(int column, const QVariant &value, int role)
536 {
537     if (column != 0)
538         return BufferItem::setData(column, value, role);
539
540     switch (role) {
541     case Qt::EditRole:
542     {
543         QString newName = value.toString();
544
545         // Sanity check - buffer names must not contain newlines!
546         int nlpos = newName.indexOf('\n');
547         if (nlpos >= 0)
548             newName = newName.left(nlpos);
549
550         if (!newName.isEmpty()) {
551             Client::renameBuffer(bufferId(), newName);
552             return true;
553         }
554         else {
555             return false;
556         }
557     }
558     break;
559     default:
560         return BufferItem::setData(column, value, role);
561     }
562 }
563
564
565 void QueryBufferItem::setBufferName(const QString &name)
566 {
567     BufferItem::setBufferName(name);
568     NetworkId netId = data(0, NetworkModel::NetworkIdRole).value<NetworkId>();
569     const Network *net = Client::network(netId);
570     if (net)
571         setIrcUser(net->ircUser(name));
572 }
573
574
575 QString QueryBufferItem::toolTip(int column) const
576 {
577     // pretty much code duplication of IrcUserItem::toolTip() but inheritance won't solve this...
578     Q_UNUSED(column);
579     QString strTooltip;
580     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
581     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
582
583     // Keep track of whether or not information has been added
584     bool infoAdded = false;
585
586     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
587     tooltip << "<p class='bold' align='center'>";
588     tooltip << tr("Query with %1").arg(NetworkItem::escapeHTML(bufferName(), true));
589     if (!_ircUser) {
590         // User seems to be offline, let the no information message be added below
591         tooltip << "</p>";
592     } else {
593         // Function to add a row to the tooltip table
594         auto addRow = [&](const QString& key, const QString& value, bool condition) {
595             if (condition) {
596                 tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
597                 infoAdded = true;
598             }
599         };
600
601         // User information is available
602         if (_ircUser->userModes() != "") {
603             //TODO Translate user Modes and add them to the table below and in IrcUserItem::toolTip
604             tooltip << " (" << _ircUser->userModes() << ")";
605         }
606         tooltip << "</p>";
607
608         tooltip << "<table cellspacing='5' cellpadding='0'>";
609         if (_ircUser->isAway()) {
610             QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
611
612             // If away message is known, replace with the escaped message.
613             if (!_ircUser->awayMessage().isEmpty()) {
614                 awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
615             }
616             addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
617         }
618         addRow(tr("Realname"),
619                NetworkItem::escapeHTML(_ircUser->realName()),
620                !_ircUser->realName().isEmpty());
621         // suserHost may return "<nick> is available for help", which should be translated.
622         // See https://www.alien.net.au/irc/irc2numerics.html
623         if(_ircUser->suserHost().endsWith("available for help")) {
624             addRow(NetworkItem::escapeHTML(tr("Help status"), true),
625                    NetworkItem::escapeHTML(tr("Available for help")),
626                    true);
627         } else {
628             addRow(NetworkItem::escapeHTML(tr("Service status"), true),
629                    NetworkItem::escapeHTML(_ircUser->suserHost()),
630                    !_ircUser->suserHost().isEmpty());
631         }
632
633         // Keep track of whether or not the account information's been added.  Don't show it twice.
634         bool accountAdded = false;
635         if(!_ircUser->account().isEmpty()) {
636             // IRCv3 account-notify is supported by the core and IRC server.
637             // Assume logged out (seems to be more common)
638             QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
639
640             // If account is logged in, replace with the escaped account name.
641             if (_ircUser->account() != "*") {
642                 accountHTML = NetworkItem::escapeHTML(_ircUser->account());
643             }
644             addRow(NetworkItem::escapeHTML(tr("Account"), true),
645                    accountHTML,
646                    true);
647             // Mark the row as added
648             accountAdded = true;
649         }
650         // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
651         // See https://www.alien.net.au/irc/irc2numerics.html
652         if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
653             addRow(NetworkItem::escapeHTML(tr("Account"), true),
654                    NetworkItem::escapeHTML(tr("Identified for this nick")),
655                    !accountAdded);
656             // Don't add the account row again if information's already added via account-notify
657             // Not used further down...
658             // accountAdded = true;
659         } else {
660             addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
661                    NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
662                    !_ircUser->whoisServiceReply().isEmpty());
663         }
664         addRow(tr("Hostmask"),
665                NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
666                !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
667         // ircOperator may contain "is an" or "is a", which should be removed.
668         addRow(tr("Operator"),
669                NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
670                !_ircUser->ircOperator().isEmpty());
671
672         if (_ircUser->idleTime().isValid()) {
673             QDateTime now = QDateTime::currentDateTime();
674             QDateTime idle = _ircUser->idleTime();
675             int idleTime = idle.secsTo(now);
676             addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
677         }
678
679         if (_ircUser->loginTime().isValid()) {
680             addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
681         }
682
683         addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
684         tooltip << "</table>";
685     }
686
687     // If no further information found, offer an explanatory message
688     if (!infoAdded)
689         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
690
691     tooltip << "</qt>";
692     return strTooltip;
693 }
694
695
696 void QueryBufferItem::setIrcUser(IrcUser *ircUser)
697 {
698     if (_ircUser == ircUser)
699         return;
700
701     if (_ircUser) {
702         disconnect(_ircUser, 0, this, 0);
703     }
704
705     if (ircUser) {
706         connect(ircUser, SIGNAL(destroyed(QObject*)), SLOT(removeIrcUser()));
707         connect(ircUser, SIGNAL(quited()), this, SLOT(removeIrcUser()));
708         connect(ircUser, SIGNAL(awaySet(bool)), this, SIGNAL(dataChanged()));
709         connect(ircUser, SIGNAL(encryptedSet(bool)), this, SLOT(setEncrypted(bool)));
710     }
711
712     _ircUser = ircUser;
713     emit dataChanged();
714 }
715
716
717 void QueryBufferItem::removeIrcUser()
718 {
719     if (_ircUser) {
720         // Disconnect the active IrcUser before removing it, otherwise it will fire removeIrcUser()
721         // a second time when the object's destroyed due to QueryBufferItem::setIrcUser() connecting
722         // SIGNAL destroyed(QObject*) to SLOT removeIrcUser().
723         // This fixes removing an active IrcUser if the user had quit then rejoined in a nonstandard
724         // manner (e.g. updateNickFromHost calling newIrcUser, triggered by an away-notify message).
725         disconnect(_ircUser, 0, this, 0);
726
727         // Clear IrcUser (only set to 0 if not already 0)
728         _ircUser = 0;
729
730         // Only emit dataChanged() if data actually changed.  This might serve as a small
731         // optimization, but it can be moved outside the if statement if other behavior depends on
732         // it always being called.
733         emit dataChanged();
734     }
735 }
736
737
738 /*****************************************
739 *  ChannelBufferItem
740 *****************************************/
741 ChannelBufferItem::ChannelBufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent)
742     : BufferItem(bufferInfo, parent),
743     _ircChannel(0)
744 {
745     setFlags(flags() | Qt::ItemIsDropEnabled);
746 }
747
748
749 QVariant ChannelBufferItem::data(int column, int role) const
750 {
751     switch (role) {
752     case NetworkModel::IrcChannelRole:
753         return QVariant::fromValue<QObject *>(_ircChannel);
754     default:
755         return BufferItem::data(column, role);
756     }
757 }
758
759
760 QString ChannelBufferItem::toolTip(int column) const
761 {
762     Q_UNUSED(column);
763     QString strTooltip;
764     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
765     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
766
767     // Function to add a row to the tooltip table
768     auto addRow = [&](const QString& key, const QString& value, bool condition) {
769         if (condition) {
770             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
771         }
772     };
773
774     tooltip << "<p class='bold' align='center'>";
775     tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "</p>";
776
777     if (isActive()) {
778         tooltip << "<table cellspacing='5' cellpadding='0'>";
779         addRow(tr("Users"), QString::number(nickCount()), true);
780
781         if (_ircChannel) {
782             QString channelMode = _ircChannel->channelModeString(); // channelModeString is compiled on the fly -> thus cache the result
783             if (!channelMode.isEmpty())
784                 addRow(tr("Mode"), channelMode, true);
785         }
786
787         ItemViewSettings s;
788         bool showTopic = s.displayTopicInTooltip();
789         if (showTopic) {
790             QString _topic = topic();
791             if (_topic != "") {
792                 _topic = stripFormatCodes(_topic);
793                 _topic = NetworkItem::escapeHTML(_topic);
794                 addRow(tr("Topic"), _topic, true);
795             }
796         }
797
798         tooltip << "</table>";
799     } else {
800         tooltip << "<p class='italic' align='center'>" << tr("Not active, double-click to join") << "</p>";
801     }
802
803     tooltip << "</qt>";
804     return strTooltip;
805 }
806
807
808 void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel)
809 {
810     if (_ircChannel) {
811         qWarning() << Q_FUNC_INFO << "IrcChannel already set; cleanup failed!?";
812         disconnect(_ircChannel, 0, this, 0);
813     }
814
815     _ircChannel = ircChannel;
816
817     connect(ircChannel, SIGNAL(destroyed(QObject*)),
818         this, SLOT(ircChannelDestroyed()));
819     connect(ircChannel, SIGNAL(topicSet(QString)),
820         this, SLOT(setTopic(QString)));
821     connect(ircChannel, SIGNAL(encryptedSet(bool)),
822         this, SLOT(setEncrypted(bool)));
823     connect(ircChannel, SIGNAL(ircUsersJoined(QList<IrcUser *> )),
824         this, SLOT(join(QList<IrcUser *> )));
825     connect(ircChannel, SIGNAL(ircUserParted(IrcUser *)),
826         this, SLOT(part(IrcUser *)));
827     connect(ircChannel, SIGNAL(parted()),
828         this, SLOT(ircChannelParted()));
829     connect(ircChannel, SIGNAL(ircUserModesSet(IrcUser *, QString)),
830         this, SLOT(userModeChanged(IrcUser *)));
831     connect(ircChannel, SIGNAL(ircUserModeAdded(IrcUser *, QString)),
832         this, SLOT(userModeChanged(IrcUser *)));
833     connect(ircChannel, SIGNAL(ircUserModeRemoved(IrcUser *, QString)),
834         this, SLOT(userModeChanged(IrcUser *)));
835
836     if (!ircChannel->ircUsers().isEmpty())
837         join(ircChannel->ircUsers());
838
839     emit dataChanged();
840 }
841
842 QString ChannelBufferItem::nickChannelModes(const QString &nick) const
843 {
844     if (!_ircChannel) {
845         qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes";
846         return QString();
847     }
848
849     return _ircChannel->userModes(nick);
850 }
851
852
853 void ChannelBufferItem::ircChannelParted()
854 {
855     Q_CHECK_PTR(_ircChannel);
856     disconnect(_ircChannel, 0, this, 0);
857     _ircChannel = 0;
858     emit dataChanged();
859     removeAllChilds();
860 }
861
862
863 void ChannelBufferItem::ircChannelDestroyed()
864 {
865     if (_ircChannel) {
866         _ircChannel = 0;
867         emit dataChanged();
868         removeAllChilds();
869     }
870 }
871
872
873 void ChannelBufferItem::join(const QList<IrcUser *> &ircUsers)
874 {
875     addUsersToCategory(ircUsers);
876     emit dataChanged(2);
877 }
878
879
880 UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId)
881 {
882     UserCategoryItem *categoryItem = 0;
883
884     for (int i = 0; i < childCount(); i++) {
885         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
886         if (!categoryItem)
887             continue;
888         if (categoryItem->categoryId() == categoryId)
889             return categoryItem;
890     }
891     return 0;
892 }
893
894
895 void ChannelBufferItem::addUserToCategory(IrcUser *ircUser)
896 {
897     addUsersToCategory(QList<IrcUser *>() << ircUser);
898 }
899
900
901 void ChannelBufferItem::addUsersToCategory(const QList<IrcUser *> &ircUsers)
902 {
903     Q_ASSERT(_ircChannel);
904
905     QHash<UserCategoryItem *, QList<IrcUser *> > categories;
906
907     int categoryId = -1;
908     UserCategoryItem *categoryItem = 0;
909
910     foreach(IrcUser *ircUser, ircUsers) {
911         categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
912         categoryItem = findCategoryItem(categoryId);
913         if (!categoryItem) {
914             categoryItem = new UserCategoryItem(categoryId, this);
915             categories[categoryItem] = QList<IrcUser *>();
916             newChild(categoryItem);
917         }
918         categories[categoryItem] << ircUser;
919     }
920
921     QHash<UserCategoryItem *, QList<IrcUser *> >::const_iterator catIter = categories.constBegin();
922     while (catIter != categories.constEnd()) {
923         catIter.key()->addUsers(catIter.value());
924         ++catIter;
925     }
926 }
927
928
929 void ChannelBufferItem::part(IrcUser *ircUser)
930 {
931     if (!ircUser) {
932         qWarning() << bufferName() << "ChannelBufferItem::part(): unknown User" << ircUser;
933         return;
934     }
935
936     disconnect(ircUser, 0, this, 0);
937     removeUserFromCategory(ircUser);
938     emit dataChanged(2);
939 }
940
941
942 void ChannelBufferItem::removeUserFromCategory(IrcUser *ircUser)
943 {
944     if (!_ircChannel) {
945         // If we parted the channel there might still be some ircUsers connected.
946         // in that case we just ignore the call
947         Q_ASSERT(childCount() == 0);
948         return;
949     }
950
951     UserCategoryItem *categoryItem = 0;
952     for (int i = 0; i < childCount(); i++) {
953         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
954         if (categoryItem->removeUser(ircUser)) {
955             if (categoryItem->childCount() == 0)
956                 removeChild(i);
957             break;
958         }
959     }
960 }
961
962
963 void ChannelBufferItem::userModeChanged(IrcUser *ircUser)
964 {
965     Q_ASSERT(_ircChannel);
966
967     int categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
968     UserCategoryItem *categoryItem = findCategoryItem(categoryId);
969
970     if (categoryItem) {
971         if (categoryItem->findIrcUser(ircUser)) {
972             return; // already in the right category;
973         }
974     }
975     else {
976         categoryItem = new UserCategoryItem(categoryId, this);
977         newChild(categoryItem);
978     }
979
980     // find the item that needs reparenting
981     IrcUserItem *ircUserItem = 0;
982     for (int i = 0; i < childCount(); i++) {
983         UserCategoryItem *oldCategoryItem = qobject_cast<UserCategoryItem *>(child(i));
984         Q_ASSERT(oldCategoryItem);
985         IrcUserItem *userItem = oldCategoryItem->findIrcUser(ircUser);
986         if (userItem) {
987             ircUserItem = userItem;
988             break;
989         }
990     }
991
992     if (!ircUserItem) {
993         qWarning() << "ChannelBufferItem::userModeChanged(IrcUser *): unable to determine old category of" << ircUser;
994         return;
995     }
996     ircUserItem->reParent(categoryItem);
997 }
998
999
1000 /*****************************************
1001 *  User Category Items (like @vh etc.)
1002 *****************************************/
1003 // we hardcode this even though we have PREFIX in network... but that wouldn't help with mapping modes to
1004 // category strings anyway.
1005 const QList<QChar> UserCategoryItem::categories = QList<QChar>() << 'q' << 'a' << 'o' << 'h' << 'v';
1006
1007 UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent)
1008     : PropertyMapItem(parent),
1009     _category(category)
1010 {
1011     setFlags(Qt::ItemIsEnabled);
1012     setTreeItemFlags(AbstractTreeItem::DeleteOnLastChildRemoved);
1013     setObjectName(parent->data(0, Qt::DisplayRole).toString() + "/" + QString::number(category));
1014 }
1015
1016
1017 QStringList UserCategoryItem::propertyOrder() const
1018 {
1019     static QStringList order{"categoryName"};
1020     return order;
1021 }
1022
1023
1024 // caching this makes no sense, since we display the user number dynamically
1025 QString UserCategoryItem::categoryName() const
1026 {
1027     int n = childCount();
1028     switch (_category) {
1029     case 0:
1030         return tr("%n Owner(s)", 0, n);
1031     case 1:
1032         return tr("%n Admin(s)", 0, n);
1033     case 2:
1034         return tr("%n Operator(s)", 0, n);
1035     case 3:
1036         return tr("%n Half-Op(s)", 0, n);
1037     case 4:
1038         return tr("%n Voiced", 0, n);
1039     default:
1040         return tr("%n User(s)", 0, n);
1041     }
1042 }
1043
1044
1045 IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser)
1046 {
1047     IrcUserItem *userItem = 0;
1048
1049     for (int i = 0; i < childCount(); i++) {
1050         userItem = qobject_cast<IrcUserItem *>(child(i));
1051         if (!userItem)
1052             continue;
1053         if (userItem->ircUser() == ircUser)
1054             return userItem;
1055     }
1056     return 0;
1057 }
1058
1059
1060 void UserCategoryItem::addUsers(const QList<IrcUser *> &ircUsers)
1061 {
1062     QList<AbstractTreeItem *> userItems;
1063     foreach(IrcUser *ircUser, ircUsers)
1064     userItems << new IrcUserItem(ircUser, this);
1065     newChilds(userItems);
1066     emit dataChanged(0);
1067 }
1068
1069
1070 bool UserCategoryItem::removeUser(IrcUser *ircUser)
1071 {
1072     IrcUserItem *userItem = findIrcUser(ircUser);
1073     bool success = (bool)userItem;
1074     if (success) {
1075         removeChild(userItem);
1076         emit dataChanged(0);
1077     }
1078     return success;
1079 }
1080
1081
1082 int UserCategoryItem::categoryFromModes(const QString &modes)
1083 {
1084     for (int i = 0; i < categories.count(); i++) {
1085         if (modes.contains(categories[i]))
1086             return i;
1087     }
1088     return categories.count();
1089 }
1090
1091
1092 QVariant UserCategoryItem::data(int column, int role) const
1093 {
1094     switch (role) {
1095     case TreeModel::SortRole:
1096         return _category;
1097     case NetworkModel::ItemActiveRole:
1098         return true;
1099     case NetworkModel::ItemTypeRole:
1100         return NetworkModel::UserCategoryItemType;
1101     case NetworkModel::BufferIdRole:
1102         return parent()->data(column, role);
1103     case NetworkModel::NetworkIdRole:
1104         return parent()->data(column, role);
1105     case NetworkModel::BufferInfoRole:
1106         return parent()->data(column, role);
1107     default:
1108         return PropertyMapItem::data(column, role);
1109     }
1110 }
1111
1112
1113 /*****************************************
1114 *  Irc User Items
1115 *****************************************/
1116 IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent)
1117     : PropertyMapItem(parent),
1118     _ircUser(ircUser)
1119 {
1120     setObjectName(ircUser->nick());
1121     connect(ircUser, SIGNAL(quited()), this, SLOT(ircUserQuited()));
1122     connect(ircUser, SIGNAL(nickSet(QString)), this, SIGNAL(dataChanged()));
1123     connect(ircUser, SIGNAL(awaySet(bool)), this, SIGNAL(dataChanged()));
1124 }
1125
1126
1127 QStringList IrcUserItem::propertyOrder() const
1128 {
1129     static QStringList order{"nickName"};
1130     return order;
1131 }
1132
1133
1134 QVariant IrcUserItem::data(int column, int role) const
1135 {
1136     switch (role) {
1137     case NetworkModel::ItemActiveRole:
1138         return isActive();
1139     case NetworkModel::ItemTypeRole:
1140         return NetworkModel::IrcUserItemType;
1141     case NetworkModel::BufferIdRole:
1142         return parent()->data(column, role);
1143     case NetworkModel::NetworkIdRole:
1144         return parent()->data(column, role);
1145     case NetworkModel::BufferInfoRole:
1146         return parent()->data(column, role);
1147     case NetworkModel::IrcChannelRole:
1148         return parent()->data(column, role);
1149     case NetworkModel::IrcUserRole:
1150         return QVariant::fromValue<QObject *>(_ircUser.data());
1151     case NetworkModel::UserAwayRole:
1152         return (bool)_ircUser ? _ircUser->isAway() : false;
1153     default:
1154         return PropertyMapItem::data(column, role);
1155     }
1156 }
1157
1158
1159 QString IrcUserItem::toolTip(int column) const
1160 {
1161     Q_UNUSED(column);
1162     QString strTooltip;
1163     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
1164     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
1165
1166     // Keep track of whether or not information has been added
1167     bool infoAdded = false;
1168
1169     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
1170     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(nickName(), true);
1171     if (_ircUser->userModes() != "") {
1172         //TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip
1173         tooltip << " (" << _ircUser->userModes() << ")";
1174     }
1175     tooltip << "</p>";
1176
1177     auto addRow = [&](const QString& key, const QString& value, bool condition) {
1178         if (condition)
1179         {
1180             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
1181             infoAdded = true;
1182         }
1183     };
1184
1185     tooltip << "<table cellspacing='5' cellpadding='0'>";
1186     addRow(tr("Modes"),
1187            NetworkItem::escapeHTML(channelModes()),
1188            !channelModes().isEmpty());
1189     if (_ircUser->isAway()) {
1190         QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
1191
1192         // If away message is known, replace with the escaped message.
1193         if (!_ircUser->awayMessage().isEmpty()) {
1194             awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
1195         }
1196         addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
1197     }
1198     addRow(tr("Realname"),
1199            NetworkItem::escapeHTML(_ircUser->realName()),
1200            !_ircUser->realName().isEmpty());
1201
1202     // suserHost may return "<nick> is available for help", which should be translated.
1203     // See https://www.alien.net.au/irc/irc2numerics.html
1204     if(_ircUser->suserHost().endsWith("available for help")) {
1205         addRow(NetworkItem::escapeHTML(tr("Help status"), true),
1206                NetworkItem::escapeHTML(tr("Available for help")),
1207                true);
1208     } else {
1209         addRow(NetworkItem::escapeHTML(tr("Service status"), true),
1210                NetworkItem::escapeHTML(_ircUser->suserHost()),
1211                !_ircUser->suserHost().isEmpty());
1212     }
1213
1214     // Keep track of whether or not the account information's been added.  Don't show it twice.
1215     bool accountAdded = false;
1216     if(!_ircUser->account().isEmpty()) {
1217         // IRCv3 account-notify is supported by the core and IRC server.
1218         // Assume logged out (seems to be more common)
1219         QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
1220
1221         // If account is logged in, replace with the escaped account name.
1222         if (_ircUser->account() != "*") {
1223             accountHTML = NetworkItem::escapeHTML(_ircUser->account());
1224         }
1225         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1226                accountHTML,
1227                true);
1228         // Mark the row as added
1229         accountAdded = true;
1230     }
1231     // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
1232     // See https://www.alien.net.au/irc/irc2numerics.html
1233     if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
1234         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1235                NetworkItem::escapeHTML(tr("Identified for this nick")),
1236                !accountAdded);
1237         // Don't add the account row again if information's already added via account-notify
1238         // Not used further down...
1239         // accountAdded = true;
1240     } else {
1241         addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
1242                NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
1243                !_ircUser->whoisServiceReply().isEmpty());
1244     }
1245     addRow(tr("Hostmask"),
1246            NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
1247            !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
1248     // ircOperator may contain "is an" or "is a", which should be removed.
1249     addRow(tr("Operator"),
1250            NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
1251            !_ircUser->ircOperator().isEmpty());
1252
1253     if (_ircUser->idleTime().isValid()) {
1254         QDateTime now = QDateTime::currentDateTime();
1255         QDateTime idle = _ircUser->idleTime();
1256         int idleTime = idle.secsTo(now);
1257         addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
1258     }
1259
1260     if (_ircUser->loginTime().isValid()) {
1261         addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
1262     }
1263
1264     addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
1265     tooltip << "</table>";
1266
1267     // If no further information found, offer an explanatory message
1268     if (!infoAdded)
1269         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
1270
1271     tooltip << "</qt>";
1272     return strTooltip;
1273 }
1274
1275 QString IrcUserItem::channelModes() const
1276 {
1277     // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem.
1278     // We want the channel buffer item in order to get the channel-specific user modes.
1279     UserCategoryItem *category = qobject_cast<UserCategoryItem *>(parent());
1280     if (!category)
1281         return QString();
1282
1283     ChannelBufferItem *channel = qobject_cast<ChannelBufferItem *>(category->parent());
1284     if (!channel)
1285         return QString();
1286
1287     return channel->nickChannelModes(nickName());
1288 }
1289
1290
1291 /*****************************************
1292  * NetworkModel
1293  *****************************************/
1294 NetworkModel::NetworkModel(QObject *parent)
1295     : TreeModel(NetworkModel::defaultHeader(), parent)
1296 {
1297     connect(this, SIGNAL(rowsInserted(const QModelIndex &, int, int)),
1298         this, SLOT(checkForNewBuffers(const QModelIndex &, int, int)));
1299     connect(this, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)),
1300         this, SLOT(checkForRemovedBuffers(const QModelIndex &, int, int)));
1301
1302     BufferSettings defaultSettings;
1303     defaultSettings.notify("UserNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1304     defaultSettings.notify("ServerNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1305     defaultSettings.notify("ErrorMsgsTarget", this, SLOT(messageRedirectionSettingsChanged()));
1306     messageRedirectionSettingsChanged();
1307 }
1308
1309
1310 QList<QVariant> NetworkModel::defaultHeader()
1311 {
1312     QList<QVariant> data;
1313     data << tr("Chat") << tr("Topic") << tr("Nick Count");
1314     return data;
1315 }
1316
1317
1318 bool NetworkModel::isBufferIndex(const QModelIndex &index) const
1319 {
1320     return index.data(NetworkModel::ItemTypeRole) == NetworkModel::BufferItemType;
1321 }
1322
1323
1324 int NetworkModel::networkRow(NetworkId networkId) const
1325 {
1326     NetworkItem *netItem = 0;
1327     for (int i = 0; i < rootItem->childCount(); i++) {
1328         netItem = qobject_cast<NetworkItem *>(rootItem->child(i));
1329         if (!netItem)
1330             continue;
1331         if (netItem->networkId() == networkId)
1332             return i;
1333     }
1334     return -1;
1335 }
1336
1337
1338 QModelIndex NetworkModel::networkIndex(NetworkId networkId)
1339 {
1340     int netRow = networkRow(networkId);
1341     if (netRow == -1)
1342         return QModelIndex();
1343     else
1344         return indexByItem(qobject_cast<NetworkItem *>(rootItem->child(netRow)));
1345 }
1346
1347
1348 NetworkItem *NetworkModel::findNetworkItem(NetworkId networkId) const
1349 {
1350     int netRow = networkRow(networkId);
1351     if (netRow == -1)
1352         return 0;
1353     else
1354         return qobject_cast<NetworkItem *>(rootItem->child(netRow));
1355 }
1356
1357
1358 NetworkItem *NetworkModel::networkItem(NetworkId networkId)
1359 {
1360     NetworkItem *netItem = findNetworkItem(networkId);
1361
1362     if (netItem == 0) {
1363         netItem = new NetworkItem(networkId, rootItem);
1364         rootItem->newChild(netItem);
1365     }
1366     return netItem;
1367 }
1368
1369
1370 void NetworkModel::networkRemoved(const NetworkId &networkId)
1371 {
1372     int netRow = networkRow(networkId);
1373     if (netRow != -1) {
1374         rootItem->removeChild(netRow);
1375     }
1376 }
1377
1378
1379 QModelIndex NetworkModel::bufferIndex(BufferId bufferId)
1380 {
1381     if (!_bufferItemCache.contains(bufferId))
1382         return QModelIndex();
1383
1384     return indexByItem(_bufferItemCache[bufferId]);
1385 }
1386
1387
1388 BufferItem *NetworkModel::findBufferItem(BufferId bufferId) const
1389 {
1390     if (_bufferItemCache.contains(bufferId))
1391         return _bufferItemCache[bufferId];
1392     else
1393         return 0;
1394 }
1395
1396
1397 BufferItem *NetworkModel::bufferItem(const BufferInfo &bufferInfo)
1398 {
1399     if (_bufferItemCache.contains(bufferInfo.bufferId()))
1400         return _bufferItemCache[bufferInfo.bufferId()];
1401
1402     NetworkItem *netItem = networkItem(bufferInfo.networkId());
1403     return netItem->bufferItem(bufferInfo);
1404 }
1405
1406
1407 QStringList NetworkModel::mimeTypes() const
1408 {
1409     // mimetypes we accept for drops
1410     QStringList types;
1411     // comma separated list of colon separated pairs of networkid and bufferid
1412     // example: 0:1,0:2,1:4
1413     types << "application/Quassel/BufferItemList";
1414     return types;
1415 }
1416
1417
1418 bool NetworkModel::mimeContainsBufferList(const QMimeData *mimeData)
1419 {
1420     return mimeData->hasFormat("application/Quassel/BufferItemList");
1421 }
1422
1423
1424 QList<QPair<NetworkId, BufferId> > NetworkModel::mimeDataToBufferList(const QMimeData *mimeData)
1425 {
1426     QList<QPair<NetworkId, BufferId> > bufferList;
1427
1428     if (!mimeContainsBufferList(mimeData))
1429         return bufferList;
1430
1431     QStringList rawBufferList = QString::fromLatin1(mimeData->data("application/Quassel/BufferItemList")).split(",");
1432     NetworkId networkId;
1433     BufferId bufferUid;
1434     foreach(QString rawBuffer, rawBufferList) {
1435         if (!rawBuffer.contains(":"))
1436             continue;
1437         networkId = rawBuffer.section(":", 0, 0).toInt();
1438         bufferUid = rawBuffer.section(":", 1, 1).toInt();
1439         bufferList.append(qMakePair(networkId, bufferUid));
1440     }
1441     return bufferList;
1442 }
1443
1444
1445 QMimeData *NetworkModel::mimeData(const QModelIndexList &indexes) const
1446 {
1447     QMimeData *mimeData = new QMimeData();
1448
1449     QStringList bufferlist;
1450     QString netid, uid, bufferid;
1451     foreach(QModelIndex index, indexes) {
1452         netid = QString::number(index.data(NetworkIdRole).value<NetworkId>().toInt());
1453         uid = QString::number(index.data(BufferIdRole).value<BufferId>().toInt());
1454         bufferid = QString("%1:%2").arg(netid).arg(uid);
1455         if (!bufferlist.contains(bufferid))
1456             bufferlist << bufferid;
1457     }
1458
1459     mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toLatin1());
1460
1461     return mimeData;
1462 }
1463
1464
1465 void NetworkModel::attachNetwork(Network *net)
1466 {
1467     NetworkItem *netItem = networkItem(net->networkId());
1468     netItem->attachNetwork(net);
1469 }
1470
1471
1472 void NetworkModel::bufferUpdated(BufferInfo bufferInfo)
1473 {
1474     BufferItem *bufItem = bufferItem(bufferInfo);
1475     QModelIndex itemindex = indexByItem(bufItem);
1476     emit dataChanged(itemindex, itemindex);
1477 }
1478
1479
1480 void NetworkModel::removeBuffer(BufferId bufferId)
1481 {
1482     BufferItem *buffItem = findBufferItem(bufferId);
1483     if (!buffItem)
1484         return;
1485
1486     buffItem->parent()->removeChild(buffItem);
1487 }
1488
1489
1490 MsgId NetworkModel::lastSeenMsgId(BufferId bufferId) const
1491 {
1492     if (!_bufferItemCache.contains(bufferId))
1493         return MsgId();
1494
1495     return _bufferItemCache[bufferId]->lastSeenMsgId();
1496 }
1497
1498
1499 MsgId NetworkModel::markerLineMsgId(BufferId bufferId) const
1500 {
1501     if (!_bufferItemCache.contains(bufferId))
1502         return MsgId();
1503
1504     return _bufferItemCache[bufferId]->markerLineMsgId();
1505 }
1506
1507
1508 // FIXME we always seem to use this (expensive) non-const version
1509 MsgId NetworkModel::lastSeenMsgId(const BufferId &bufferId)
1510 {
1511     BufferItem *bufferItem = findBufferItem(bufferId);
1512     if (!bufferItem) {
1513         qDebug() << "NetworkModel::lastSeenMsgId(): buffer is unknown:" << bufferId;
1514         Client::purgeKnownBufferIds();
1515         return MsgId();
1516     }
1517     return bufferItem->lastSeenMsgId();
1518 }
1519
1520
1521 void NetworkModel::setLastSeenMsgId(const BufferId &bufferId, const MsgId &msgId)
1522 {
1523     BufferItem *bufferItem = findBufferItem(bufferId);
1524     if (!bufferItem) {
1525         qDebug() << "NetworkModel::setLastSeenMsgId(): buffer is unknown:" << bufferId;
1526         Client::purgeKnownBufferIds();
1527         return;
1528     }
1529     bufferItem->setLastSeenMsgId(msgId);
1530     emit lastSeenMsgSet(bufferId, msgId);
1531 }
1532
1533
1534 void NetworkModel::setMarkerLineMsgId(const BufferId &bufferId, const MsgId &msgId)
1535 {
1536     BufferItem *bufferItem = findBufferItem(bufferId);
1537     if (!bufferItem) {
1538         qDebug() << "NetworkModel::setMarkerLineMsgId(): buffer is unknown:" << bufferId;
1539         Client::purgeKnownBufferIds();
1540         return;
1541     }
1542     bufferItem->setMarkerLineMsgId(msgId);
1543     emit markerLineSet(bufferId, msgId);
1544 }
1545
1546
1547 void NetworkModel::updateBufferActivity(Message &msg)
1548 {
1549     int redirectionTarget = 0;
1550     switch (msg.type()) {
1551     case Message::Notice:
1552         if (bufferType(msg.bufferId()) != BufferInfo::ChannelBuffer) {
1553             msg.setFlags(msg.flags() | Message::Redirected);
1554             if (msg.flags() & Message::ServerMsg) {
1555                 // server notice
1556                 redirectionTarget = _serverNoticesTarget;
1557             }
1558             else {
1559                 redirectionTarget = _userNoticesTarget;
1560             }
1561         }
1562         break;
1563     case Message::Error:
1564         msg.setFlags(msg.flags() | Message::Redirected);
1565         redirectionTarget = _errorMsgsTarget;
1566         break;
1567     // Update IrcUser's last activity
1568     case Message::Plain:
1569     case Message::Action:
1570         if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) {
1571             const Network *net = Client::network(msg.bufferInfo().networkId());
1572             IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : 0;
1573             if (user)
1574                 user->setLastChannelActivity(msg.bufferId(), msg.timestamp());
1575         }
1576         break;
1577     default:
1578         break;
1579     }
1580
1581     if (msg.flags() & Message::Redirected) {
1582         if (redirectionTarget & BufferSettings::DefaultBuffer)
1583             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1584
1585         if (redirectionTarget & BufferSettings::StatusBuffer) {
1586             const NetworkItem *netItem = findNetworkItem(msg.bufferInfo().networkId());
1587             if (netItem) {
1588                 updateBufferActivity(netItem->statusBufferItem(), msg);
1589             }
1590         }
1591     }
1592     else {
1593         if ((BufferSettings(msg.bufferId()).messageFilter() & msg.type()) != msg.type())
1594             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1595     }
1596 }
1597
1598
1599 void NetworkModel::updateBufferActivity(BufferItem *bufferItem, const Message &msg)
1600 {
1601     if (!bufferItem)
1602         return;
1603
1604     bufferItem->updateActivityLevel(msg);
1605     if (bufferItem->isCurrentBuffer())
1606         emit requestSetLastSeenMsg(bufferItem->bufferId(), msg.msgId());
1607 }
1608
1609
1610 void NetworkModel::setBufferActivity(const BufferId &bufferId, BufferInfo::ActivityLevel level)
1611 {
1612     BufferItem *bufferItem = findBufferItem(bufferId);
1613     if (!bufferItem) {
1614         qDebug() << "NetworkModel::setBufferActivity(): buffer is unknown:" << bufferId;
1615         return;
1616     }
1617     bufferItem->setActivityLevel(level);
1618 }
1619
1620
1621 void NetworkModel::clearBufferActivity(const BufferId &bufferId)
1622 {
1623     BufferItem *bufferItem = findBufferItem(bufferId);
1624     if (!bufferItem) {
1625         qDebug() << "NetworkModel::clearBufferActivity(): buffer is unknown:" << bufferId;
1626         return;
1627     }
1628     bufferItem->clearActivityLevel();
1629 }
1630
1631
1632 const Network *NetworkModel::networkByIndex(const QModelIndex &index) const
1633 {
1634     QVariant netVariant = index.data(NetworkIdRole);
1635     if (!netVariant.isValid())
1636         return 0;
1637
1638     NetworkId networkId = netVariant.value<NetworkId>();
1639     return Client::network(networkId);
1640 }
1641
1642
1643 void NetworkModel::checkForRemovedBuffers(const QModelIndex &parent, int start, int end)
1644 {
1645     if (parent.data(ItemTypeRole) != NetworkItemType)
1646         return;
1647
1648     for (int row = start; row <= end; row++) {
1649         _bufferItemCache.remove(parent.child(row, 0).data(BufferIdRole).value<BufferId>());
1650     }
1651 }
1652
1653
1654 void NetworkModel::checkForNewBuffers(const QModelIndex &parent, int start, int end)
1655 {
1656     if (parent.data(ItemTypeRole) != NetworkItemType)
1657         return;
1658
1659     for (int row = start; row <= end; row++) {
1660         QModelIndex child = parent.child(row, 0);
1661         _bufferItemCache[child.data(BufferIdRole).value < BufferId > ()] = static_cast<BufferItem *>(child.internalPointer());
1662     }
1663 }
1664
1665
1666 QString NetworkModel::bufferName(BufferId bufferId) const
1667 {
1668     if (!_bufferItemCache.contains(bufferId))
1669         return QString();
1670
1671     return _bufferItemCache[bufferId]->bufferName();
1672 }
1673
1674
1675 BufferInfo::Type NetworkModel::bufferType(BufferId bufferId) const
1676 {
1677     if (!_bufferItemCache.contains(bufferId))
1678         return BufferInfo::InvalidBuffer;
1679
1680     return _bufferItemCache[bufferId]->bufferType();
1681 }
1682
1683
1684 BufferInfo NetworkModel::bufferInfo(BufferId bufferId) const
1685 {
1686     if (!_bufferItemCache.contains(bufferId))
1687         return BufferInfo();
1688
1689     return _bufferItemCache[bufferId]->bufferInfo();
1690 }
1691
1692
1693 NetworkId NetworkModel::networkId(BufferId bufferId) const
1694 {
1695     if (!_bufferItemCache.contains(bufferId))
1696         return NetworkId();
1697
1698     NetworkItem *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1699     if (netItem)
1700         return netItem->networkId();
1701     else
1702         return NetworkId();
1703 }
1704
1705
1706 QString NetworkModel::networkName(BufferId bufferId) const
1707 {
1708     if (!_bufferItemCache.contains(bufferId))
1709         return QString();
1710
1711     NetworkItem *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1712     if (netItem)
1713         return netItem->networkName();
1714     else
1715         return QString();
1716 }
1717
1718
1719 BufferId NetworkModel::bufferId(NetworkId networkId, const QString &bufferName, Qt::CaseSensitivity cs) const
1720 {
1721     const NetworkItem *netItem = findNetworkItem(networkId);
1722     if (!netItem)
1723         return BufferId();
1724
1725     for (int i = 0; i < netItem->childCount(); i++) {
1726         BufferItem *bufferItem = qobject_cast<BufferItem *>(netItem->child(i));
1727         if (bufferItem && !bufferItem->bufferName().compare(bufferName, cs))
1728             return bufferItem->bufferId();
1729     }
1730     return BufferId();
1731 }
1732
1733
1734 void NetworkModel::sortBufferIds(QList<BufferId> &bufferIds) const
1735 {
1736     QList<BufferItem *> bufferItems;
1737     foreach(BufferId bufferId, bufferIds) {
1738         if (_bufferItemCache.contains(bufferId))
1739             bufferItems << _bufferItemCache[bufferId];
1740     }
1741
1742     qSort(bufferItems.begin(), bufferItems.end(), bufferItemLessThan);
1743
1744     bufferIds.clear();
1745     foreach(BufferItem *bufferItem, bufferItems) {
1746         bufferIds << bufferItem->bufferId();
1747     }
1748 }
1749
1750
1751 QList<BufferId> NetworkModel::allBufferIdsSorted() const
1752 {
1753     QList<BufferId> bufferIds = allBufferIds();
1754     sortBufferIds(bufferIds);
1755     return bufferIds;
1756 }
1757
1758
1759 bool NetworkModel::bufferItemLessThan(const BufferItem *left, const BufferItem *right)
1760 {
1761     int leftType = left->bufferType();
1762     int rightType = right->bufferType();
1763
1764     if (leftType != rightType)
1765         return leftType < rightType;
1766     else
1767         return QString::compare(left->bufferName(), right->bufferName(), Qt::CaseInsensitive) < 0;
1768 }
1769
1770
1771 void NetworkModel::messageRedirectionSettingsChanged()
1772 {
1773     BufferSettings bufferSettings;
1774
1775     _userNoticesTarget = bufferSettings.userNoticesTarget();
1776     _serverNoticesTarget = bufferSettings.serverNoticesTarget();
1777     _errorMsgsTarget = bufferSettings.errorMsgsTarget();
1778 }
1779
1780 void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity) {
1781     auto _bufferItem = findBufferItem(bufferId);
1782     if (!_bufferItem) {
1783         qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId;
1784         return;
1785     }
1786     auto hiddenTypes = BufferSettings(bufferId).messageFilter();
1787     auto visibleTypes = ~hiddenTypes;
1788     auto activityVisibleTypesIntersection = activity & visibleTypes;
1789     _bufferItem->setActivity(activityVisibleTypesIntersection, false);
1790 }
1791
1792 void NetworkModel::highlightCountChanged(BufferId bufferId, int count) {
1793     auto _bufferItem = findBufferItem(bufferId);
1794     if (!_bufferItem) {
1795         qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId;
1796         return;
1797     }
1798     _bufferItem->addActivity(Message::Types{}, count > 0);
1799 }