modernize: Replace most remaining old-style connects by PMF ones
[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, &NetworkItem::networkDataChanged, this, &NetworkItem::dataChanged);
50     connect(this, &NetworkItem::beginRemoveChilds, this, &NetworkItem::onBeginRemoveChilds);
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, &NetworkItem::networkDataChanged, this, &NetworkItem::dataChanged);
119         connect(this, &NetworkItem::networkDataChanged, bufferItem, &BufferItem::dataChanged);
120         connect(bufferItem, &BufferItem::dataChanged, this, &NetworkItem::dataChanged);
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         auto *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, &Network::networkNameSet,
170         this, &NetworkItem::setNetworkName);
171     connect(network, &Network::currentServerSet,
172         this, &NetworkItem::setCurrentServer);
173     connect(network, &Network::ircChannelAdded,
174         this, &NetworkItem::attachIrcChannel);
175     connect(network, &Network::ircUserAdded,
176         this, &NetworkItem::attachIrcUser);
177     connect(network, &Network::connectedSet,
178         this, [this]() { emit networkDataChanged(); });
179     connect(network, &QObject::destroyed,
180         this, &NetworkItem::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         auto *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     auto *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, &IrcUser::destroyed, this, &QueryBufferItem::removeIrcUser);
702         connect(ircUser, &IrcUser::quited, this, &QueryBufferItem::removeIrcUser);
703         connect(ircUser, &IrcUser::awaySet, this, [this]() { emit dataChanged(); });
704         connect(ircUser, &IrcUser::encryptedSet, this, &BufferItem::setEncrypted);
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, &QObject::destroyed, this, &ChannelBufferItem::ircChannelDestroyed);
813     connect(ircChannel, &IrcChannel::topicSet, this, &ChannelBufferItem::setTopic);
814     connect(ircChannel, &IrcChannel::encryptedSet, this, &ChannelBufferItem::setEncrypted);
815     connect(ircChannel, &IrcChannel::ircUsersJoined, this, &ChannelBufferItem::join);
816     connect(ircChannel, &IrcChannel::ircUserParted, this, &ChannelBufferItem::part);
817     connect(ircChannel, &IrcChannel::parted, this, &ChannelBufferItem::ircChannelParted);
818     connect(ircChannel, &IrcChannel::ircUserModesSet, this, &ChannelBufferItem::userModeChanged);
819     connect(ircChannel, &IrcChannel::ircUserModeAdded, this, &ChannelBufferItem::userModeChanged);
820     connect(ircChannel, &IrcChannel::ircUserModeRemoved, this, &ChannelBufferItem::userModeChanged);
821
822     if (!ircChannel->ircUsers().isEmpty())
823         join(ircChannel->ircUsers());
824
825     emit dataChanged();
826 }
827
828 QString ChannelBufferItem::nickChannelModes(const QString &nick) const
829 {
830     if (!_ircChannel) {
831         qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes";
832         return QString();
833     }
834
835     return _ircChannel->userModes(nick);
836 }
837
838
839 void ChannelBufferItem::ircChannelParted()
840 {
841     Q_CHECK_PTR(_ircChannel);
842     disconnect(_ircChannel, nullptr, this, nullptr);
843     _ircChannel = nullptr;
844     emit dataChanged();
845     removeAllChilds();
846 }
847
848
849 void ChannelBufferItem::ircChannelDestroyed()
850 {
851     if (_ircChannel) {
852         _ircChannel = nullptr;
853         emit dataChanged();
854         removeAllChilds();
855     }
856 }
857
858
859 void ChannelBufferItem::join(const QList<IrcUser *> &ircUsers)
860 {
861     addUsersToCategory(ircUsers);
862     emit dataChanged(2);
863 }
864
865
866 UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId)
867 {
868     UserCategoryItem *categoryItem = nullptr;
869
870     for (int i = 0; i < childCount(); i++) {
871         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
872         if (!categoryItem)
873             continue;
874         if (categoryItem->categoryId() == categoryId)
875             return categoryItem;
876     }
877     return nullptr;
878 }
879
880
881 void ChannelBufferItem::addUserToCategory(IrcUser *ircUser)
882 {
883     addUsersToCategory(QList<IrcUser *>() << ircUser);
884 }
885
886
887 void ChannelBufferItem::addUsersToCategory(const QList<IrcUser *> &ircUsers)
888 {
889     Q_ASSERT(_ircChannel);
890
891     QHash<UserCategoryItem *, QList<IrcUser *> > categories;
892
893     int categoryId = -1;
894     UserCategoryItem *categoryItem = nullptr;
895
896     foreach(IrcUser *ircUser, ircUsers) {
897         categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
898         categoryItem = findCategoryItem(categoryId);
899         if (!categoryItem) {
900             categoryItem = new UserCategoryItem(categoryId, this);
901             categories[categoryItem] = QList<IrcUser *>();
902             newChild(categoryItem);
903         }
904         categories[categoryItem] << ircUser;
905     }
906
907     QHash<UserCategoryItem *, QList<IrcUser *> >::const_iterator catIter = categories.constBegin();
908     while (catIter != categories.constEnd()) {
909         catIter.key()->addUsers(catIter.value());
910         ++catIter;
911     }
912 }
913
914
915 void ChannelBufferItem::part(IrcUser *ircUser)
916 {
917     if (!ircUser) {
918         qWarning() << bufferName() << "ChannelBufferItem::part(): unknown User" << ircUser;
919         return;
920     }
921
922     disconnect(ircUser, nullptr, this, nullptr);
923     removeUserFromCategory(ircUser);
924     emit dataChanged(2);
925 }
926
927
928 void ChannelBufferItem::removeUserFromCategory(IrcUser *ircUser)
929 {
930     if (!_ircChannel) {
931         // If we parted the channel there might still be some ircUsers connected.
932         // in that case we just ignore the call
933         Q_ASSERT(childCount() == 0);
934         return;
935     }
936
937     UserCategoryItem *categoryItem = nullptr;
938     for (int i = 0; i < childCount(); i++) {
939         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
940         if (categoryItem->removeUser(ircUser)) {
941             if (categoryItem->childCount() == 0)
942                 removeChild(i);
943             break;
944         }
945     }
946 }
947
948
949 void ChannelBufferItem::userModeChanged(IrcUser *ircUser)
950 {
951     Q_ASSERT(_ircChannel);
952
953     int categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
954     UserCategoryItem *categoryItem = findCategoryItem(categoryId);
955
956     if (categoryItem) {
957         if (categoryItem->findIrcUser(ircUser)) {
958             return; // already in the right category;
959         }
960     }
961     else {
962         categoryItem = new UserCategoryItem(categoryId, this);
963         newChild(categoryItem);
964     }
965
966     // find the item that needs reparenting
967     IrcUserItem *ircUserItem = nullptr;
968     for (int i = 0; i < childCount(); i++) {
969         auto *oldCategoryItem = qobject_cast<UserCategoryItem *>(child(i));
970         Q_ASSERT(oldCategoryItem);
971         IrcUserItem *userItem = oldCategoryItem->findIrcUser(ircUser);
972         if (userItem) {
973             ircUserItem = userItem;
974             break;
975         }
976     }
977
978     if (!ircUserItem) {
979         qWarning() << "ChannelBufferItem::userModeChanged(IrcUser *): unable to determine old category of" << ircUser;
980         return;
981     }
982     ircUserItem->reParent(categoryItem);
983 }
984
985
986 /*****************************************
987 *  User Category Items (like @vh etc.)
988 *****************************************/
989 // we hardcode this even though we have PREFIX in network... but that wouldn't help with mapping modes to
990 // category strings anyway.
991 const QList<QChar> UserCategoryItem::categories = QList<QChar>() << 'q' << 'a' << 'o' << 'h' << 'v';
992
993 UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent)
994     : PropertyMapItem(parent),
995     _category(category)
996 {
997     setFlags(Qt::ItemIsEnabled);
998     setTreeItemFlags(AbstractTreeItem::DeleteOnLastChildRemoved);
999     setObjectName(parent->data(0, Qt::DisplayRole).toString() + "/" + QString::number(category));
1000 }
1001
1002
1003 QStringList UserCategoryItem::propertyOrder() const
1004 {
1005     static QStringList order{"categoryName"};
1006     return order;
1007 }
1008
1009
1010 // caching this makes no sense, since we display the user number dynamically
1011 QString UserCategoryItem::categoryName() const
1012 {
1013     int n = childCount();
1014     switch (_category) {
1015     case 0:
1016         return tr("%n Owner(s)", "", n);
1017     case 1:
1018         return tr("%n Admin(s)", "", n);
1019     case 2:
1020         return tr("%n Operator(s)", "", n);
1021     case 3:
1022         return tr("%n Half-Op(s)", "", n);
1023     case 4:
1024         return tr("%n Voiced", "", n);
1025     default:
1026         return tr("%n User(s)", "", n);
1027     }
1028 }
1029
1030
1031 IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser)
1032 {
1033     IrcUserItem *userItem = nullptr;
1034
1035     for (int i = 0; i < childCount(); i++) {
1036         userItem = qobject_cast<IrcUserItem *>(child(i));
1037         if (!userItem)
1038             continue;
1039         if (userItem->ircUser() == ircUser)
1040             return userItem;
1041     }
1042     return nullptr;
1043 }
1044
1045
1046 void UserCategoryItem::addUsers(const QList<IrcUser *> &ircUsers)
1047 {
1048     QList<AbstractTreeItem *> userItems;
1049     foreach(IrcUser *ircUser, ircUsers)
1050     userItems << new IrcUserItem(ircUser, this);
1051     newChilds(userItems);
1052     emit dataChanged(0);
1053 }
1054
1055
1056 bool UserCategoryItem::removeUser(IrcUser *ircUser)
1057 {
1058     IrcUserItem *userItem = findIrcUser(ircUser);
1059     auto success = (bool)userItem;
1060     if (success) {
1061         removeChild(userItem);
1062         emit dataChanged(0);
1063     }
1064     return success;
1065 }
1066
1067
1068 int UserCategoryItem::categoryFromModes(const QString &modes)
1069 {
1070     for (int i = 0; i < categories.count(); i++) {
1071         if (modes.contains(categories[i]))
1072             return i;
1073     }
1074     return categories.count();
1075 }
1076
1077
1078 QVariant UserCategoryItem::data(int column, int role) const
1079 {
1080     switch (role) {
1081     case TreeModel::SortRole:
1082         return _category;
1083     case NetworkModel::ItemActiveRole:
1084         return true;
1085     case NetworkModel::ItemTypeRole:
1086         return NetworkModel::UserCategoryItemType;
1087     case NetworkModel::BufferIdRole:
1088         return parent()->data(column, role);
1089     case NetworkModel::NetworkIdRole:
1090         return parent()->data(column, role);
1091     case NetworkModel::BufferInfoRole:
1092         return parent()->data(column, role);
1093     default:
1094         return PropertyMapItem::data(column, role);
1095     }
1096 }
1097
1098
1099 /*****************************************
1100 *  Irc User Items
1101 *****************************************/
1102 IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent)
1103     : PropertyMapItem(parent),
1104     _ircUser(ircUser)
1105 {
1106     setObjectName(ircUser->nick());
1107     connect(ircUser, &IrcUser::quited, this, &IrcUserItem::ircUserQuited);
1108     connect(ircUser, &IrcUser::nickSet, this, [this]() { emit dataChanged(); });
1109     connect(ircUser, &IrcUser::awaySet, this, [this]() { emit dataChanged(); });
1110 }
1111
1112
1113 QStringList IrcUserItem::propertyOrder() const
1114 {
1115     static QStringList order{"nickName"};
1116     return order;
1117 }
1118
1119
1120 QVariant IrcUserItem::data(int column, int role) const
1121 {
1122     switch (role) {
1123     case NetworkModel::ItemActiveRole:
1124         return isActive();
1125     case NetworkModel::ItemTypeRole:
1126         return NetworkModel::IrcUserItemType;
1127     case NetworkModel::BufferIdRole:
1128         return parent()->data(column, role);
1129     case NetworkModel::NetworkIdRole:
1130         return parent()->data(column, role);
1131     case NetworkModel::BufferInfoRole:
1132         return parent()->data(column, role);
1133     case NetworkModel::IrcChannelRole:
1134         return parent()->data(column, role);
1135     case NetworkModel::IrcUserRole:
1136         return QVariant::fromValue<QObject *>(_ircUser.data());
1137     case NetworkModel::UserAwayRole:
1138         return (bool)_ircUser ? _ircUser->isAway() : false;
1139     default:
1140         return PropertyMapItem::data(column, role);
1141     }
1142 }
1143
1144
1145 QString IrcUserItem::toolTip(int column) const
1146 {
1147     Q_UNUSED(column);
1148     QString strTooltip;
1149     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
1150     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
1151
1152     // Keep track of whether or not information has been added
1153     bool infoAdded = false;
1154
1155     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
1156     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(nickName(), true);
1157     if (_ircUser->userModes() != "") {
1158         //TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip
1159         tooltip << " (" << _ircUser->userModes() << ")";
1160     }
1161     tooltip << "</p>";
1162
1163     auto addRow = [&](const QString& key, const QString& value, bool condition) {
1164         if (condition)
1165         {
1166             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
1167             infoAdded = true;
1168         }
1169     };
1170
1171     tooltip << "<table cellspacing='5' cellpadding='0'>";
1172     addRow(tr("Modes"),
1173            NetworkItem::escapeHTML(channelModes()),
1174            !channelModes().isEmpty());
1175     if (_ircUser->isAway()) {
1176         QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
1177
1178         // If away message is known, replace with the escaped message.
1179         if (!_ircUser->awayMessage().isEmpty()) {
1180             awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
1181         }
1182         addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
1183     }
1184     addRow(tr("Realname"),
1185            NetworkItem::escapeHTML(_ircUser->realName()),
1186            !_ircUser->realName().isEmpty());
1187
1188     // suserHost may return "<nick> is available for help", which should be translated.
1189     // See https://www.alien.net.au/irc/irc2numerics.html
1190     if(_ircUser->suserHost().endsWith("available for help")) {
1191         addRow(NetworkItem::escapeHTML(tr("Help status"), true),
1192                NetworkItem::escapeHTML(tr("Available for help")),
1193                true);
1194     } else {
1195         addRow(NetworkItem::escapeHTML(tr("Service status"), true),
1196                NetworkItem::escapeHTML(_ircUser->suserHost()),
1197                !_ircUser->suserHost().isEmpty());
1198     }
1199
1200     // Keep track of whether or not the account information's been added.  Don't show it twice.
1201     bool accountAdded = false;
1202     if(!_ircUser->account().isEmpty()) {
1203         // IRCv3 account-notify is supported by the core and IRC server.
1204         // Assume logged out (seems to be more common)
1205         QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
1206
1207         // If account is logged in, replace with the escaped account name.
1208         if (_ircUser->account() != "*") {
1209             accountHTML = NetworkItem::escapeHTML(_ircUser->account());
1210         }
1211         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1212                accountHTML,
1213                true);
1214         // Mark the row as added
1215         accountAdded = true;
1216     }
1217     // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
1218     // See https://www.alien.net.au/irc/irc2numerics.html
1219     if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
1220         addRow(NetworkItem::escapeHTML(tr("Account"), true),
1221                NetworkItem::escapeHTML(tr("Identified for this nick")),
1222                !accountAdded);
1223         // Don't add the account row again if information's already added via account-notify
1224         // Not used further down...
1225         // accountAdded = true;
1226     } else {
1227         addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
1228                NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
1229                !_ircUser->whoisServiceReply().isEmpty());
1230     }
1231     addRow(tr("Hostmask"),
1232            NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
1233            !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
1234     // ircOperator may contain "is an" or "is a", which should be removed.
1235     addRow(tr("Operator"),
1236            NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
1237            !_ircUser->ircOperator().isEmpty());
1238
1239     if (_ircUser->idleTime().isValid()) {
1240         QDateTime now = QDateTime::currentDateTime();
1241         QDateTime idle = _ircUser->idleTime();
1242         int idleTime = idle.secsTo(now);
1243         addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
1244     }
1245
1246     if (_ircUser->loginTime().isValid()) {
1247         addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
1248     }
1249
1250     addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
1251     tooltip << "</table>";
1252
1253     // If no further information found, offer an explanatory message
1254     if (!infoAdded)
1255         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
1256
1257     tooltip << "</qt>";
1258     return strTooltip;
1259 }
1260
1261 QString IrcUserItem::channelModes() const
1262 {
1263     // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem.
1264     // We want the channel buffer item in order to get the channel-specific user modes.
1265     auto *category = qobject_cast<UserCategoryItem *>(parent());
1266     if (!category)
1267         return QString();
1268
1269     auto *channel = qobject_cast<ChannelBufferItem *>(category->parent());
1270     if (!channel)
1271         return QString();
1272
1273     return channel->nickChannelModes(nickName());
1274 }
1275
1276
1277 /*****************************************
1278  * NetworkModel
1279  *****************************************/
1280 NetworkModel::NetworkModel(QObject *parent)
1281     : TreeModel(NetworkModel::defaultHeader(), parent)
1282 {
1283     connect(this, &NetworkModel::rowsInserted, this, &NetworkModel::checkForNewBuffers);
1284     connect(this, &NetworkModel::rowsAboutToBeRemoved, this, &NetworkModel::checkForRemovedBuffers);
1285
1286     BufferSettings defaultSettings;
1287     defaultSettings.notify("UserNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1288     defaultSettings.notify("ServerNoticesTarget", this, SLOT(messageRedirectionSettingsChanged()));
1289     defaultSettings.notify("ErrorMsgsTarget", this, SLOT(messageRedirectionSettingsChanged()));
1290     messageRedirectionSettingsChanged();
1291 }
1292
1293
1294 QList<QVariant> NetworkModel::defaultHeader()
1295 {
1296     QList<QVariant> data;
1297     data << tr("Chat") << tr("Topic") << tr("Nick Count");
1298     return data;
1299 }
1300
1301
1302 bool NetworkModel::isBufferIndex(const QModelIndex &index) const
1303 {
1304     return index.data(NetworkModel::ItemTypeRole) == NetworkModel::BufferItemType;
1305 }
1306
1307
1308 int NetworkModel::networkRow(NetworkId networkId) const
1309 {
1310     NetworkItem *netItem = nullptr;
1311     for (int i = 0; i < rootItem->childCount(); i++) {
1312         netItem = qobject_cast<NetworkItem *>(rootItem->child(i));
1313         if (!netItem)
1314             continue;
1315         if (netItem->networkId() == networkId)
1316             return i;
1317     }
1318     return -1;
1319 }
1320
1321
1322 QModelIndex NetworkModel::networkIndex(NetworkId networkId)
1323 {
1324     int netRow = networkRow(networkId);
1325     if (netRow == -1)
1326         return {};
1327     else
1328         return indexByItem(qobject_cast<NetworkItem *>(rootItem->child(netRow)));
1329 }
1330
1331
1332 NetworkItem *NetworkModel::findNetworkItem(NetworkId networkId) const
1333 {
1334     int netRow = networkRow(networkId);
1335     if (netRow == -1)
1336         return nullptr;
1337     else
1338         return qobject_cast<NetworkItem *>(rootItem->child(netRow));
1339 }
1340
1341
1342 NetworkItem *NetworkModel::networkItem(NetworkId networkId)
1343 {
1344     NetworkItem *netItem = findNetworkItem(networkId);
1345
1346     if (netItem == nullptr) {
1347         netItem = new NetworkItem(networkId, rootItem);
1348         rootItem->newChild(netItem);
1349     }
1350     return netItem;
1351 }
1352
1353
1354 void NetworkModel::networkRemoved(const NetworkId &networkId)
1355 {
1356     int netRow = networkRow(networkId);
1357     if (netRow != -1) {
1358         rootItem->removeChild(netRow);
1359     }
1360 }
1361
1362
1363 QModelIndex NetworkModel::bufferIndex(BufferId bufferId)
1364 {
1365     if (!_bufferItemCache.contains(bufferId))
1366         return {};
1367
1368     return indexByItem(_bufferItemCache[bufferId]);
1369 }
1370
1371
1372 BufferItem *NetworkModel::findBufferItem(BufferId bufferId) const
1373 {
1374     if (_bufferItemCache.contains(bufferId))
1375         return _bufferItemCache[bufferId];
1376     else
1377         return nullptr;
1378 }
1379
1380
1381 BufferItem *NetworkModel::bufferItem(const BufferInfo &bufferInfo)
1382 {
1383     if (_bufferItemCache.contains(bufferInfo.bufferId()))
1384         return _bufferItemCache[bufferInfo.bufferId()];
1385
1386     NetworkItem *netItem = networkItem(bufferInfo.networkId());
1387     return netItem->bufferItem(bufferInfo);
1388 }
1389
1390
1391 QStringList NetworkModel::mimeTypes() const
1392 {
1393     // mimetypes we accept for drops
1394     QStringList types;
1395     // comma separated list of colon separated pairs of networkid and bufferid
1396     // example: 0:1,0:2,1:4
1397     types << "application/Quassel/BufferItemList";
1398     return types;
1399 }
1400
1401
1402 bool NetworkModel::mimeContainsBufferList(const QMimeData *mimeData)
1403 {
1404     return mimeData->hasFormat("application/Quassel/BufferItemList");
1405 }
1406
1407
1408 QList<QPair<NetworkId, BufferId> > NetworkModel::mimeDataToBufferList(const QMimeData *mimeData)
1409 {
1410     QList<QPair<NetworkId, BufferId> > bufferList;
1411
1412     if (!mimeContainsBufferList(mimeData))
1413         return bufferList;
1414
1415     QStringList rawBufferList = QString::fromLatin1(mimeData->data("application/Quassel/BufferItemList")).split(",");
1416     NetworkId networkId;
1417     BufferId bufferUid;
1418     foreach(QString rawBuffer, rawBufferList) {
1419         if (!rawBuffer.contains(":"))
1420             continue;
1421         networkId = rawBuffer.section(":", 0, 0).toInt();
1422         bufferUid = rawBuffer.section(":", 1, 1).toInt();
1423         bufferList.append(qMakePair(networkId, bufferUid));
1424     }
1425     return bufferList;
1426 }
1427
1428
1429 QMimeData *NetworkModel::mimeData(const QModelIndexList &indexes) const
1430 {
1431     auto *mimeData = new QMimeData();
1432
1433     QStringList bufferlist;
1434     QString netid, uid, bufferid;
1435     foreach(QModelIndex index, indexes) {
1436         netid = QString::number(index.data(NetworkIdRole).value<NetworkId>().toInt());
1437         uid = QString::number(index.data(BufferIdRole).value<BufferId>().toInt());
1438         bufferid = QString("%1:%2").arg(netid).arg(uid);
1439         if (!bufferlist.contains(bufferid))
1440             bufferlist << bufferid;
1441     }
1442
1443     mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toLatin1());
1444
1445     return mimeData;
1446 }
1447
1448
1449 void NetworkModel::attachNetwork(Network *net)
1450 {
1451     NetworkItem *netItem = networkItem(net->networkId());
1452     netItem->attachNetwork(net);
1453 }
1454
1455
1456 void NetworkModel::bufferUpdated(BufferInfo bufferInfo)
1457 {
1458     BufferItem *bufItem = bufferItem(bufferInfo);
1459     QModelIndex itemindex = indexByItem(bufItem);
1460     emit dataChanged(itemindex, itemindex);
1461 }
1462
1463
1464 void NetworkModel::removeBuffer(BufferId bufferId)
1465 {
1466     BufferItem *buffItem = findBufferItem(bufferId);
1467     if (!buffItem)
1468         return;
1469
1470     buffItem->parent()->removeChild(buffItem);
1471 }
1472
1473
1474 MsgId NetworkModel::lastSeenMsgId(BufferId bufferId) const
1475 {
1476     if (!_bufferItemCache.contains(bufferId))
1477         return {};
1478
1479     return _bufferItemCache[bufferId]->lastSeenMsgId();
1480 }
1481
1482
1483 MsgId NetworkModel::markerLineMsgId(BufferId bufferId) const
1484 {
1485     if (!_bufferItemCache.contains(bufferId))
1486         return {};
1487
1488     return _bufferItemCache[bufferId]->markerLineMsgId();
1489 }
1490
1491
1492 // FIXME we always seem to use this (expensive) non-const version
1493 MsgId NetworkModel::lastSeenMsgId(const BufferId &bufferId)
1494 {
1495     BufferItem *bufferItem = findBufferItem(bufferId);
1496     if (!bufferItem) {
1497         qDebug() << "NetworkModel::lastSeenMsgId(): buffer is unknown:" << bufferId;
1498         Client::purgeKnownBufferIds();
1499         return {};
1500     }
1501     return bufferItem->lastSeenMsgId();
1502 }
1503
1504
1505 void NetworkModel::setLastSeenMsgId(const BufferId &bufferId, const MsgId &msgId)
1506 {
1507     BufferItem *bufferItem = findBufferItem(bufferId);
1508     if (!bufferItem) {
1509         qDebug() << "NetworkModel::setLastSeenMsgId(): buffer is unknown:" << bufferId;
1510         Client::purgeKnownBufferIds();
1511         return;
1512     }
1513     bufferItem->setLastSeenMsgId(msgId);
1514     emit lastSeenMsgSet(bufferId, msgId);
1515 }
1516
1517
1518 void NetworkModel::setMarkerLineMsgId(const BufferId &bufferId, const MsgId &msgId)
1519 {
1520     BufferItem *bufferItem = findBufferItem(bufferId);
1521     if (!bufferItem) {
1522         qDebug() << "NetworkModel::setMarkerLineMsgId(): buffer is unknown:" << bufferId;
1523         Client::purgeKnownBufferIds();
1524         return;
1525     }
1526     bufferItem->setMarkerLineMsgId(msgId);
1527     emit markerLineSet(bufferId, msgId);
1528 }
1529
1530
1531 void NetworkModel::updateBufferActivity(Message &msg)
1532 {
1533     int redirectionTarget = 0;
1534     switch (msg.type()) {
1535     case Message::Notice:
1536         if (bufferType(msg.bufferId()) != BufferInfo::ChannelBuffer) {
1537             msg.setFlags(msg.flags() | Message::Redirected);
1538             if (msg.flags() & Message::ServerMsg) {
1539                 // server notice
1540                 redirectionTarget = _serverNoticesTarget;
1541             }
1542             else {
1543                 redirectionTarget = _userNoticesTarget;
1544             }
1545         }
1546         break;
1547     case Message::Error:
1548         msg.setFlags(msg.flags() | Message::Redirected);
1549         redirectionTarget = _errorMsgsTarget;
1550         break;
1551     // Update IrcUser's last activity
1552     case Message::Plain:
1553     case Message::Action:
1554         if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) {
1555             const Network *net = Client::network(msg.bufferInfo().networkId());
1556             IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : nullptr;
1557             if (user)
1558                 user->setLastChannelActivity(msg.bufferId(), msg.timestamp());
1559         }
1560         break;
1561     default:
1562         break;
1563     }
1564
1565     if (msg.flags() & Message::Redirected) {
1566         if (redirectionTarget & BufferSettings::DefaultBuffer)
1567             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1568
1569         if (redirectionTarget & BufferSettings::StatusBuffer) {
1570             const NetworkItem *netItem = findNetworkItem(msg.bufferInfo().networkId());
1571             if (netItem) {
1572                 updateBufferActivity(netItem->statusBufferItem(), msg);
1573             }
1574         }
1575     }
1576     else {
1577         if ((BufferSettings(msg.bufferId()).messageFilter() & msg.type()) != msg.type())
1578             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1579     }
1580 }
1581
1582
1583 void NetworkModel::updateBufferActivity(BufferItem *bufferItem, const Message &msg)
1584 {
1585     if (!bufferItem)
1586         return;
1587
1588     bufferItem->updateActivityLevel(msg);
1589     if (bufferItem->isCurrentBuffer())
1590         emit requestSetLastSeenMsg(bufferItem->bufferId(), msg.msgId());
1591 }
1592
1593
1594 void NetworkModel::setBufferActivity(const BufferId &bufferId, BufferInfo::ActivityLevel level)
1595 {
1596     BufferItem *bufferItem = findBufferItem(bufferId);
1597     if (!bufferItem) {
1598         qDebug() << "NetworkModel::setBufferActivity(): buffer is unknown:" << bufferId;
1599         return;
1600     }
1601     bufferItem->setActivityLevel(level);
1602 }
1603
1604
1605 void NetworkModel::clearBufferActivity(const BufferId &bufferId)
1606 {
1607     BufferItem *bufferItem = findBufferItem(bufferId);
1608     if (!bufferItem) {
1609         qDebug() << "NetworkModel::clearBufferActivity(): buffer is unknown:" << bufferId;
1610         return;
1611     }
1612     bufferItem->clearActivityLevel();
1613 }
1614
1615
1616 const Network *NetworkModel::networkByIndex(const QModelIndex &index) const
1617 {
1618     QVariant netVariant = index.data(NetworkIdRole);
1619     if (!netVariant.isValid())
1620         return nullptr;
1621
1622     NetworkId networkId = netVariant.value<NetworkId>();
1623     return Client::network(networkId);
1624 }
1625
1626
1627 void NetworkModel::checkForRemovedBuffers(const QModelIndex &parent, int start, int end)
1628 {
1629     if (parent.data(ItemTypeRole) != NetworkItemType)
1630         return;
1631
1632     for (int row = start; row <= end; row++) {
1633         _bufferItemCache.remove(parent.child(row, 0).data(BufferIdRole).value<BufferId>());
1634     }
1635 }
1636
1637
1638 void NetworkModel::checkForNewBuffers(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         QModelIndex child = parent.child(row, 0);
1645         _bufferItemCache[child.data(BufferIdRole).value < BufferId > ()] = static_cast<BufferItem *>(child.internalPointer());
1646     }
1647 }
1648
1649
1650 QString NetworkModel::bufferName(BufferId bufferId) const
1651 {
1652     if (!_bufferItemCache.contains(bufferId))
1653         return QString();
1654
1655     return _bufferItemCache[bufferId]->bufferName();
1656 }
1657
1658
1659 BufferInfo::Type NetworkModel::bufferType(BufferId bufferId) const
1660 {
1661     if (!_bufferItemCache.contains(bufferId))
1662         return BufferInfo::InvalidBuffer;
1663
1664     return _bufferItemCache[bufferId]->bufferType();
1665 }
1666
1667
1668 BufferInfo NetworkModel::bufferInfo(BufferId bufferId) const
1669 {
1670     if (!_bufferItemCache.contains(bufferId))
1671         return BufferInfo();
1672
1673     return _bufferItemCache[bufferId]->bufferInfo();
1674 }
1675
1676
1677 NetworkId NetworkModel::networkId(BufferId bufferId) const
1678 {
1679     if (!_bufferItemCache.contains(bufferId))
1680         return {};
1681
1682     auto *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1683     if (netItem)
1684         return netItem->networkId();
1685     else
1686         return {};
1687 }
1688
1689
1690 QString NetworkModel::networkName(BufferId bufferId) const
1691 {
1692     if (!_bufferItemCache.contains(bufferId))
1693         return QString();
1694
1695     auto *netItem = qobject_cast<NetworkItem *>(_bufferItemCache[bufferId]->parent());
1696     if (netItem)
1697         return netItem->networkName();
1698     else
1699         return QString();
1700 }
1701
1702
1703 BufferId NetworkModel::bufferId(NetworkId networkId, const QString &bufferName, Qt::CaseSensitivity cs) const
1704 {
1705     const NetworkItem *netItem = findNetworkItem(networkId);
1706     if (!netItem)
1707         return {};
1708
1709     for (int i = 0; i < netItem->childCount(); i++) {
1710         auto *bufferItem = qobject_cast<BufferItem *>(netItem->child(i));
1711         if (bufferItem && !bufferItem->bufferName().compare(bufferName, cs))
1712             return bufferItem->bufferId();
1713     }
1714     return {};
1715 }
1716
1717
1718 void NetworkModel::sortBufferIds(QList<BufferId> &bufferIds) const
1719 {
1720     QList<BufferItem *> bufferItems;
1721     foreach(BufferId bufferId, bufferIds) {
1722         if (_bufferItemCache.contains(bufferId))
1723             bufferItems << _bufferItemCache[bufferId];
1724     }
1725
1726     qSort(bufferItems.begin(), bufferItems.end(), bufferItemLessThan);
1727
1728     bufferIds.clear();
1729     foreach(BufferItem *bufferItem, bufferItems) {
1730         bufferIds << bufferItem->bufferId();
1731     }
1732 }
1733
1734
1735 QList<BufferId> NetworkModel::allBufferIdsSorted() const
1736 {
1737     QList<BufferId> bufferIds = allBufferIds();
1738     sortBufferIds(bufferIds);
1739     return bufferIds;
1740 }
1741
1742
1743 bool NetworkModel::bufferItemLessThan(const BufferItem *left, const BufferItem *right)
1744 {
1745     int leftType = left->bufferType();
1746     int rightType = right->bufferType();
1747
1748     if (leftType != rightType)
1749         return leftType < rightType;
1750     else
1751         return QString::compare(left->bufferName(), right->bufferName(), Qt::CaseInsensitive) < 0;
1752 }
1753
1754
1755 void NetworkModel::messageRedirectionSettingsChanged()
1756 {
1757     BufferSettings bufferSettings;
1758
1759     _userNoticesTarget = bufferSettings.userNoticesTarget();
1760     _serverNoticesTarget = bufferSettings.serverNoticesTarget();
1761     _errorMsgsTarget = bufferSettings.errorMsgsTarget();
1762 }
1763
1764 void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity) {
1765     auto _bufferItem = findBufferItem(bufferId);
1766     if (!_bufferItem) {
1767         qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId;
1768         return;
1769     }
1770     auto hiddenTypes = BufferSettings(bufferId).messageFilter();
1771     auto visibleTypes = ~hiddenTypes;
1772     auto activityVisibleTypesIntersection = activity & visibleTypes;
1773     _bufferItem->setActivity(activityVisibleTypesIntersection, false);
1774 }
1775
1776 void NetworkModel::highlightCountChanged(BufferId bufferId, int count) {
1777     auto _bufferItem = findBufferItem(bufferId);
1778     if (!_bufferItem) {
1779         qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId;
1780         return;
1781     }
1782     _bufferItem->addActivity(Message::Types{}, count > 0);
1783 }