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