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