mumble-voip_mumble/src/mumble/UserModel.cpp

2046 lines
58 KiB
C++

// Copyright 2009-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include "UserModel.h"
#include "Accessibility.h"
#include "Channel.h"
#include "ClientUser.h"
#include "Database.h"
#include "LCD.h"
#include "Log.h"
#include "MainWindow.h"
#ifdef USE_OVERLAY
# include "Overlay.h"
#endif
#include "ChannelListenerManager.h"
#include "ServerHandler.h"
#include "Usage.h"
#include "User.h"
#include "VolumeAdjustment.h"
#include "Global.h"
#include <QtCore/QMimeData>
#include <QtCore/QStack>
#include <QtGui/QImageReader>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QToolTip>
#include <QtWidgets/QWhatsThis>
QHash< const Channel *, ModelItem * > ModelItem::c_qhChannels;
QHash< const ClientUser *, ModelItem * > ModelItem::c_qhUsers;
QHash< const ClientUser *, QList< ModelItem * > > ModelItem::s_userProxies;
bool ModelItem::bUsersTop = false;
ModelItem::ModelItem(Channel *c) {
this->cChan = c;
this->pUser = nullptr;
this->isListener = false;
bCommentSeen = true;
c_qhChannels.insert(c, this);
parent = c_qhChannels.value(c->cParent);
iUsers = 0;
}
ModelItem::ModelItem(ClientUser *p, bool isListener) {
this->cChan = nullptr;
this->pUser = p;
this->isListener = isListener;
bCommentSeen = true;
if (isListener) {
// The way operator[] works for a QHash is that it'll insert a default-constructed
// object first, before returning a reference to it, in case there is no entry for
// the provided key yet. Thus we never have to worry about explicitly adding an empty
// list for a new user before accessing it.
s_userProxies[p] << this;
} else {
c_qhUsers.insert(p, this);
}
parent = c_qhChannels.value(p->cChannel);
iUsers = 0;
}
ModelItem::ModelItem(ModelItem *i) {
// Create a shallow clone
this->cChan = i->cChan;
this->pUser = i->pUser;
this->parent = i->parent;
this->bCommentSeen = i->bCommentSeen;
this->isListener = i->isListener;
if (pUser) {
if (isListener) {
s_userProxies[pUser] << this;
} else {
c_qhUsers.insert(pUser, this);
}
} else if (cChan)
c_qhChannels.insert(cChan, this);
iUsers = i->iUsers;
}
ModelItem::~ModelItem() {
Q_ASSERT(qlChildren.count() == 0);
if (cChan && c_qhChannels.value(cChan) == this)
c_qhChannels.remove(cChan);
if (pUser) {
if (isListener) {
s_userProxies[pUser].removeAll(this);
} else {
if (c_qhUsers.value(pUser) == this)
c_qhUsers.remove(pUser);
}
}
}
void ModelItem::wipe() {
foreach (ModelItem *i, qlChildren) {
i->wipe();
delete i;
}
qlChildren.clear();
iUsers = 0;
}
ModelItem *ModelItem::child(int idx) const {
if (!validRow(idx))
return nullptr;
return qlChildren.at(idx);
}
bool ModelItem::validRow(int idx) const {
return ((idx >= 0) && (idx < qlChildren.count()));
}
ClientUser *ModelItem::userAt(int idx) const {
if (!validRow(idx))
return nullptr;
return qlChildren.at(idx)->pUser;
}
Channel *ModelItem::channelAt(int idx) const {
if (!validRow(idx))
return nullptr;
return qlChildren.at(idx)->cChan;
}
int ModelItem::rowOf(Channel *c) const {
for (int i = 0; i < qlChildren.count(); i++)
if (qlChildren.at(i)->cChan == c)
return i;
return -1;
}
int ModelItem::rowOf(ClientUser *p, const bool lookForListener) const {
for (int i = 0; i < qlChildren.count(); i++)
if (qlChildren.at(i)->isListener == lookForListener && qlChildren.at(i)->pUser == p)
return i;
return -1;
}
int ModelItem::rowOfSelf() const {
// Root?
if (!parent)
return 0;
if (pUser)
return parent->rowOf(pUser, isListener);
else
return parent->rowOf(cChan);
}
int ModelItem::rows() const {
return qlChildren.count();
}
int ModelItem::insertIndex(Channel *c) const {
QList< Channel * > qlpc;
ModelItem *item;
int ocount = 0;
foreach (item, qlChildren) {
if (item->cChan) {
if (item->cChan != c) {
qlpc << item->cChan;
}
} else
ocount++;
}
qlpc << c;
std::sort(qlpc.begin(), qlpc.end(), Channel::lessThan);
return qlpc.indexOf(c) + (bUsersTop ? ocount : 0);
}
int ModelItem::insertIndex(ClientUser *p, bool userIsListener) const {
QList< ClientUser * > qlclientuser;
ModelItem *item;
int ocount = 0;
int listenerCount = 0;
foreach (item, qlChildren) {
if (item->pUser) {
if (item->pUser != p) {
// Make sure listeners and non-listeners are all grouped together and not mixed
if ((userIsListener && item->isListener) || (!userIsListener && !item->isListener)) {
qlclientuser << item->pUser;
}
}
if (item->isListener) {
listenerCount++;
}
} else {
ocount++;
}
}
qlclientuser << p;
std::sort(qlclientuser.begin(), qlclientuser.end(), ClientUser::lessThan);
// Make sure that the a user is always added to other users either all above or all below
// sub-channels) and also make sure that listeners are grouped together and directly above
// normal users.
return qlclientuser.indexOf(p) + (bUsersTop ? 0 : ocount) + (userIsListener ? 0 : listenerCount);
}
QString ModelItem::hash() const {
if (pUser) {
if (!pUser->qsHash.isEmpty())
return pUser->qsHash + (isListener ? QLatin1String("l") : QString());
else
return QLatin1String(sha1(pUser->qsName + (isListener ? QLatin1String("l") : QString())).toHex());
} else {
QCryptographicHash chash(QCryptographicHash::Sha1);
chash.addData(cChan->qsName.toUtf8());
chash.addData(QString::number(cChan->iId).toUtf8());
if (Global::get().sh && Global::get().sh->isRunning()) {
QString host, user, pw;
unsigned short port;
Global::get().sh->getConnectionInfo(host, port, user, pw);
chash.addData(host.toUtf8());
chash.addData(QString::number(port).toUtf8());
}
return QLatin1String(chash.result().toHex());
}
}
UserModel::UserModel(QObject *p) : QAbstractItemModel(p) {
qiTalkingOff = QIcon(QLatin1String("skin:talking_off.svg"));
qiTalkingOn = QIcon(QLatin1String("skin:talking_on.svg"));
qiTalkingMuted = QIcon(QLatin1String("skin:talking_muted.svg"));
qiTalkingShout = QIcon(QLatin1String("skin:talking_alt.svg"));
qiTalkingWhisper = QIcon(QLatin1String("skin:talking_whisper.svg"));
qiPrioritySpeaker = QIcon(QLatin1String("skin:priority_speaker.svg"));
qiRecording = QIcon(QLatin1String("skin:actions/media-record.svg"));
qiMutedPushToMute.addFile(QLatin1String("skin:muted_pushtomute.svg"));
qiMutedSelf = QIcon(QLatin1String("skin:muted_self.svg"));
qiMutedServer = QIcon(QLatin1String("skin:muted_server.svg"));
qiMutedLocal = QIcon(QLatin1String("skin:muted_local.svg"));
qiIgnoredLocal = QIcon(QLatin1String("skin:status/text-missing.svg"));
qiMutedSuppressed = QIcon(QLatin1String("skin:muted_suppressed.svg"));
qiDeafenedSelf = QIcon(QLatin1String("skin:deafened_self.svg"));
qiDeafenedServer = QIcon(QLatin1String("skin:deafened_server.svg"));
qiAuthenticated = QIcon(QLatin1String("skin:authenticated.svg"));
qiChannel = QIcon(QLatin1String("skin:channel.svg"));
qiActiveChannel = QIcon(QLatin1String("skin:channel_active.svg"));
qiLinkedChannel = QIcon(QLatin1String("skin:channel_linked.svg"));
qiFriend = QIcon(QLatin1String("skin:emblems/emblem-favorite.svg"));
qiComment = QIcon(QLatin1String("skin:comment.svg"));
qiCommentSeen = QIcon(QLatin1String("skin:comment_seen.svg"));
qiFilter = QIcon(QLatin1String("skin:filter.svg"));
qiPin = QIcon(QLatin1String("skin:pin.svg"));
qiLock_locked = QIcon(QLatin1String("skin:lock_locked.svg"));
qiLock_unlocked = QIcon(QLatin1String("skin:lock_unlocked.svg"));
qiEar = QIcon(QLatin1String("skin:ear.svg"));
ModelItem::bUsersTop = Global::get().s.bUserTop;
uiSessionComment = 0;
iChannelDescription = -1;
bClicked = false;
miRoot = new ModelItem(Channel::get(Channel::ROOT_ID));
}
UserModel::~UserModel() {
removeAll();
Q_ASSERT(ModelItem::c_qhUsers.count() == 0);
Q_ASSERT(ModelItem::c_qhChannels.count() == 1);
delete miRoot;
}
int UserModel::columnCount(const QModelIndex &) const {
return 1;
}
QModelIndex UserModel::index(int row, int column, const QModelIndex &p) const {
ModelItem *item;
QModelIndex idx = QModelIndex();
if ((row < 0) || (column < 0) || (column > 1)) {
return QModelIndex();
}
if (!p.isValid()) {
return createIndex(row, column, miRoot);
} else {
item = static_cast< ModelItem * >(p.internalPointer());
}
if (!item)
return idx;
if (!item->validRow(row))
return idx;
idx = createIndex(row, column, item->child(row));
return idx;
}
QModelIndex UserModel::index(ClientUser *p, int column) const {
ModelItem *item = ModelItem::c_qhUsers.value(p);
Q_ASSERT(p);
Q_ASSERT(item);
if (!p || !item)
return QModelIndex();
QModelIndex idx = createIndex(item->rowOfSelf(), column, item);
return idx;
}
QModelIndex UserModel::index(Channel *c, int column) const {
ModelItem *item = ModelItem::c_qhChannels.value(c);
Q_ASSERT(c);
Q_ASSERT(item);
if (!item || !c)
return QModelIndex();
QModelIndex idx = createIndex(item->rowOfSelf(), column, item);
return idx;
}
QModelIndex UserModel::index(ModelItem *item) const {
return createIndex(item->rowOfSelf(), 0, item);
}
QModelIndex UserModel::channelListenerIndex(const ClientUser *user, const Channel *channel, int column) const {
QList< ModelItem * > items = ModelItem::s_userProxies.value(user);
ModelItem *item = nullptr;
for (ModelItem *currentItem : items) {
ModelItem *parent = currentItem->parent;
if (currentItem->isListener && parent && parent->cChan == channel) {
item = currentItem;
break;
}
}
Q_ASSERT(user);
Q_ASSERT(channel);
Q_ASSERT(item);
if (!item || !channel || !user) {
return QModelIndex();
}
QModelIndex idx = createIndex(item->rowOfSelf(), column, item);
return idx;
}
QModelIndex UserModel::parent(const QModelIndex &idx) const {
if (!idx.isValid())
return QModelIndex();
ModelItem *item = static_cast< ModelItem * >(idx.internalPointer());
ModelItem *parent_item = item ? item->parent : nullptr;
if (!parent_item)
return QModelIndex();
return createIndex(parent_item->rowOfSelf(), 0, parent_item);
}
int UserModel::rowCount(const QModelIndex &p) const {
ModelItem *item;
int val = 0;
if (!p.isValid())
return 1;
else
item = static_cast< ModelItem * >(p.internalPointer());
if (!item || (p.column() != 0))
return 0;
val = item->rows();
return val;
}
QString UserModel::stringIndex(const QModelIndex &idx) const {
ModelItem *item = static_cast< ModelItem * >(idx.internalPointer());
if (!idx.isValid())
return QLatin1String("invIdx");
if (!item)
return QLatin1String("invPtr");
if (item->pUser)
return QString::fromLatin1("P:%1 [%2,%3]").arg(item->pUser->qsName).arg(idx.row()).arg(idx.column());
else
return QString::fromLatin1("C:%1 [%2,%3]").arg(item->cChan->qsName).arg(idx.row()).arg(idx.column());
}
QModelIndex UserModel::getSelectedIndex() const {
QTreeView *v = Global::get().mw->qtvUsers;
if (v) {
QItemSelectionModel *sel = v->selectionModel();
return sel->currentIndex();
}
return QModelIndex();
}
QVariant UserModel::data(const QModelIndex &idx, int role) const {
if (!idx.isValid())
return QVariant();
ModelItem *item = static_cast< ModelItem * >(idx.internalPointer());
Channel *c = item->cChan;
ClientUser *p = item->pUser;
ClientUser *pSelf = ClientUser::get(Global::get().uiSession);
if (!c && !p) {
return QVariant();
}
QVariant v = otherRoles(idx, role);
if (v.isValid())
return v;
QList< QVariant > l;
if (p) {
switch (role) {
case Qt::DecorationRole:
if (idx.column() == 0) {
if (item->isListener) {
return qiEar;
} else {
// Select the talking-state symbol to display
if (p == pSelf && p->bSelfMute) {
// This is a workaround for a bug that can lead to the user having muted him/herself but
// the talking icon is stuck at qiTalkingOn for some reason.
// Until someone figures out how to fix the root of the problem, we'll have this workaround
// to cure the symptoms of the bug.
return qiTalkingOff;
}
switch (p->tsState) {
case Settings::Talking:
return qiTalkingOn;
case Settings::MutedTalking:
return qiTalkingMuted;
case Settings::Whispering:
return qiTalkingWhisper;
case Settings::Shouting:
return qiTalkingShout;
case Settings::Passive:
default:
return qiTalkingOff;
}
}
}
break;
case Qt::FontRole:
if ((idx.column() == 0) && (p->uiSession == Global::get().uiSession)) {
QFont f = Global::get().mw->font();
f.setBold(!f.bold());
f.setItalic(item->isListener);
return f;
}
if (item->isListener) {
QFont f = Global::get().mw->font();
f.setItalic(true);
return f;
}
break;
case Qt::DisplayRole:
if (idx.column() == 0) {
// Get the channel the user/listener is in
const Channel *parentChannel = item->parent ? item->parent->cChan : nullptr;
return createDisplayString(*p, item->isListener, parentChannel);
}
// Most of the following icons are for non-listeners (as listeners are merely proxies) only
// but in order to not change the order of the icons, the condition is added to each case
// individually instead of checking it up front.
if (!p->qbaCommentHash.isEmpty() && !item->isListener)
l << (item->bCommentSeen ? qiCommentSeen : qiComment);
if (p->bPrioritySpeaker && !item->isListener)
l << qiPrioritySpeaker;
if (p->bRecording)
l << qiRecording;
// ClientUser doesn't contain a push-to-mute
// state because it isn't sent to the server.
// We can show the icon only for the local user.
if (p == pSelf && Global::get().bPushToMute && !item->isListener)
l << qiMutedPushToMute;
if (p->bMute || item->isListener)
l << qiMutedServer;
if (p->bSuppress && !item->isListener)
l << qiMutedSuppressed;
if (p->bSelfMute && !item->isListener)
l << qiMutedSelf;
if (p->bLocalMute && !item->isListener)
l << qiMutedLocal;
if (p->bLocalIgnore && !item->isListener)
l << qiIgnoredLocal;
if (p->bDeaf)
l << qiDeafenedServer;
if (p->bSelfDeaf)
l << qiDeafenedSelf;
if (p->iId >= 0 && !item->isListener)
l << qiAuthenticated;
if (!p->qsFriendName.isEmpty() && !item->isListener)
l << qiFriend;
return l;
case Qt::AccessibleTextRole:
if (item->isListener) {
return tr("Channel Listener");
}
return Mumble::Accessibility::userToText(p);
case Qt::AccessibleDescriptionRole:
if (item->isListener) {
return tr("This channel listener belongs to %1").arg(Mumble::Accessibility::userToText(p));
}
return Mumble::Accessibility::userToDescription(p);
default:
break;
}
} else {
switch (role) {
case Qt::DecorationRole:
if (idx.column() == 0) {
if (Global::get().uiSession && qsLinked.contains(c)) {
if (ClientUser::get(Global::get().uiSession)->cChannel == c)
return qiActiveChannel;
else
return qiLinkedChannel;
}
return qiChannel;
}
break;
case Qt::DisplayRole:
if (idx.column() == 0) {
if (!Global::get().s.bShowUserCount || item->iUsers == 0)
return c->qsName;
return QString::fromLatin1("%1 (%2)").arg(c->qsName).arg(item->iUsers);
}
if (!c->qbaDescHash.isEmpty())
l << (item->bCommentSeen ? qiCommentSeen : qiComment);
switch (c->m_filterMode) {
case ChannelFilterMode::HIDE:
l << (qiFilter);
break;
case ChannelFilterMode::PIN:
l << (qiPin);
break;
case ChannelFilterMode::NORMAL:
// NOOP
break;
}
// Show a lock icon for enter restricted channels
if (c->hasEnterRestrictions.load()) {
if (c->localUserCanEnter.load()) {
l << qiLock_unlocked;
} else {
l << qiLock_locked;
}
}
return l;
case Qt::FontRole:
if (Global::get().uiSession) {
Channel *home = ClientUser::get(Global::get().uiSession)->cChannel;
if ((c == home) || qsLinked.contains(c)) {
QFont f = Global::get().mw->font();
if (qsLinked.count() > 1)
f.setItalic(!f.italic());
if (c == home)
f.setBold(!f.bold());
return f;
}
}
break;
case Qt::BackgroundRole:
if ((c->iId == 0) && Global::get().sh && Global::get().sh->isStrong()) {
QColor qc(Qt::green);
qc.setAlpha(32);
return qc;
}
break;
case Qt::AccessibleTextRole:
return Mumble::Accessibility::channelToText(c);
case Qt::AccessibleDescriptionRole:
return Mumble::Accessibility::channelToDescription(c);
default:
break;
}
}
return QVariant();
}
Qt::ItemFlags UserModel::flags(const QModelIndex &idx) const {
if (!idx.isValid())
return Qt::ItemIsDropEnabled;
if (idx.column() != 0)
return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled;
}
QVariant UserModel::otherRoles(const QModelIndex &idx, int role) const {
ModelItem *item = static_cast< ModelItem * >(idx.internalPointer());
ClientUser *p = item->pUser;
Channel *c = item->cChan;
int section = idx.column();
bool isUser = p;
switch (role) {
case Qt::ToolTipRole:
const_cast< UserModel * >(this)->uiSessionComment = 0;
const_cast< UserModel * >(this)->iChannelDescription = -1;
const_cast< UserModel * >(this)->bClicked = false;
switch (section) {
case 0: {
if (isUser) {
QString qsImage;
if (!p->qbaTextureHash.isEmpty()) {
if (p->qbaTexture.isEmpty()) {
p->qbaTexture = Global::get().db->blob(p->qbaTextureHash);
if (p->qbaTexture.isEmpty()) {
MumbleProto::RequestBlob mprb;
mprb.add_session_texture(p->uiSession);
Global::get().sh->sendMessage(mprb);
} else {
#ifdef USE_OVERLAY
Global::get().o->verifyTexture(p);
#endif
}
}
if (!p->qbaTexture.isEmpty()) {
QBuffer qb(&p->qbaTexture);
qb.open(QIODevice::ReadOnly);
QImageReader qir(&qb, p->qbaTextureFormat);
QSize sz = qir.size();
if (sz.width() > 0) {
qsImage = QString::fromLatin1("<img src=\"data:;base64,");
qsImage.append(QString::fromLatin1(p->qbaTexture.toBase64().toPercentEncoding()));
if (sz.width() > 128) {
int targ = sz.width() / ((sz.width() + 127) / 128);
qsImage.append(QString::fromLatin1("\" width=\"%1\" />").arg(targ));
} else {
qsImage.append(QString::fromLatin1("\" />"));
}
}
}
}
if (p->qbaCommentHash.isEmpty()) {
if (!qsImage.isEmpty())
return qsImage;
else
return p->qsName;
} else {
if (p->qsComment.isEmpty()) {
p->qsComment = QString::fromUtf8(Global::get().db->blob(p->qbaCommentHash));
if (p->qsComment.isEmpty()) {
const_cast< UserModel * >(this)->uiSessionComment = p->uiSession;
MumbleProto::RequestBlob mprb;
mprb.add_session_comment(p->uiSession);
Global::get().sh->sendMessage(mprb);
return QVariant();
}
}
const_cast< UserModel * >(this)->seenComment(idx);
QString base = Log::validHtml(p->qsComment);
if (!qsImage.isEmpty())
return QString::fromLatin1(
"<table><tr><td valign=\"top\">%1</td><td>%2</td></tr></table>")
.arg(qsImage, base);
return base;
}
} else {
if (c->qbaDescHash.isEmpty()) {
return c->qsName;
} else {
if (c->qsDesc.isEmpty()) {
c->qsDesc = QString::fromUtf8(Global::get().db->blob(c->qbaDescHash));
if (c->qsDesc.isEmpty()) {
const_cast< UserModel * >(this)->iChannelDescription = static_cast< int >(c->iId);
MumbleProto::RequestBlob mprb;
mprb.add_channel_description(c->iId);
Global::get().sh->sendMessage(mprb);
return QVariant();
}
}
const_cast< UserModel * >(this)->seenComment(idx);
return Log::validHtml(c->qsDesc);
}
}
} break;
case 1:
return isUser ? p->getFlagsString() : QVariant();
}
break;
case Qt::WhatsThisRole:
switch (section) {
case 0:
if (isUser)
return QString::fromLatin1("%1"
"<table>"
"<tr><td><img src=\"skin:talking_on.svg\" height=64 /></td><td "
"valign=\"middle\">%2</td></tr>"
"<tr><td><img src=\"skin:talking_alt.svg\" height=64 /></td><td "
"valign=\"middle\">%3</td></tr>"
"<tr><td><img src=\"skin:talking_whisper.svg\" height=64 /></td><td "
"valign=\"middle\">%4</td></tr>"
"<tr><td><img src=\"skin:talking_off.svg\" height=64 /></td><td "
"valign=\"middle\">%5</td></tr>"
"<tr><td><img src=\"skin:talking_muted.svg\" height=64 /></td><td "
"valign=\"middle\">%6</td></tr>"
"<tr><td><img src=\"skin:ear.svg\" height=64 /></td><td "
"valign=\"middle\">%7</td></tr>"
"</table>")
.arg(tr("This is a user connected to the server. The icon to the left of the user "
"indicates whether or not they are talking:"),
tr("Talking to your channel."), tr("Shouting directly to your channel."),
tr("Whispering directly to you."), tr("Not talking."),
tr("Talking while being muted on your end"),
tr("This is a channel listener. The corresponding user hears everything you say in "
"this channel."));
else
return QString::fromLatin1("%1"
"<table>"
"<tr><td><img src=\"skin:channel_active.svg\" height=64 /></td><td "
"valign=\"middle\">%2</td></tr>"
"<tr><td><img src=\"skin:channel_linked.svg\" height=64 /></td><td "
"valign=\"middle\">%3</td></tr>"
"<tr><td><img src=\"skin:channel.svg\" height=64 /></td><td "
"valign=\"middle\">%4</td></tr>"
"</table>")
.arg(tr("This is a channel on the server. The icon indicates the state of the channel:"),
tr("Your current channel."),
tr("A channel that is linked with your channel. Linked channels can talk to each "
"other."),
tr("A channel on the server that you are not linked to."));
case 1:
if (isUser)
return QString::fromLatin1("%1"
"<table>"
"<tr><td><img src=\"skin:emblems/emblem-favorite.svg\" height=64 "
"/></td><td valign=\"middle\">%2</td></tr>"
"<tr><td><img src=\"skin:authenticated.svg\" height=64 /></td><td "
"valign=\"middle\">%3</td></tr>"
"<tr><td><img src=\"skin:muted_self.svg\" height=64 /></td><td "
"valign=\"middle\">%4</td></tr>"
"<tr><td><img src=\"skin:muted_server.svg\" height=64 /></td><td "
"valign=\"middle\">%5</td></tr>"
"<tr><td><img src=\"skin:muted_suppressed.svg\" height=64 "
"/></td><td valign=\"middle\">%6</td></tr>"
"<tr><td><img src=\"skin:muted_local.svg\" height=64 /></td><td "
"valign=\"middle\">%7</td></tr>"
"<tr><td><img src=\"skin:muted_pushtomute.svg\" height=64 "
"/></td><td valign=\"middle\">%8</td></tr>"
"<tr><td><img src=\"skin:deafened_self.svg\" height=64 /></td><td "
"valign=\"middle\">%9</td></tr>"
"<tr><td><img src=\"skin:deafened_server.svg\" height=64 /></td><td "
"valign=\"middle\">%10</td></tr>"
"<tr><td><img src=\"skin:comment.svg\" height=64 /></td><td "
"valign=\"middle\">%11</td></tr>"
"<tr><td><img src=\"skin:comment_seen.svg\" height=64 /></td><td "
"valign=\"middle\">%12</td></tr>"
"<tr><td><img src=\"skin:status/text-missing.svg\" height=64 "
"/></td><td valign=\"middle\">%13</td></tr>"
"</table>")
.arg(tr("This shows the flags the user has on the server, if any:"),
tr("On your friend list"), tr("Authenticated user"),
tr("Muted (manually muted by self)"), tr("Muted (manually muted by admin)"),
tr("Muted (not allowed to speak in current channel)"),
tr("Muted (muted by you, only on your machine)"), tr("Muted (push-to-mute)"))
.arg(tr("Deafened (by self)"), tr("Deafened (by admin)"),
tr("User has a new comment set (click to show)"),
tr("User has a comment set, which you've already seen. (click to show)"),
tr("Ignoring Text Messages"));
else
return QString::fromLatin1("%1"
"<table>"
"<tr><td><img src=\"skin:comment.svg\" height=64 /></td><td "
"valign=\"middle\">%10</td></tr>"
"<tr><td><img src=\"skin:comment_seen.svg\" height=64 /></td><td "
"valign=\"middle\">%11</td></tr>"
"<tr><td><img src=\"skin:filter.svg\" height=64 /></td><td "
"valign=\"middle\">%12</td></tr>"
"<tr><td><img src=\"skin:pin.svg\" height=64 /></td><td "
"valign=\"middle\">%13</td></tr>"
"<tr><td><img src=\"skin:lock_locked.svg\" height=64 /></td><td "
"valign=\"middle\">%14</td></tr>"
"<tr><td><img src=\"skin:lock_unlocked.svg\" height=64 /></td><td "
"valign=\"middle\">%15</td></tr>"
"</table>")
.arg(tr("This shows the flags the channel has, if any:"),
tr("Channel has a new comment set (click to show)"),
tr("Channel has a comment set, which you've already seen. (click to show)"),
tr("Channel will be hidden when filtering is enabled"),
tr("Channel will be pinned when filtering is enabled"),
tr("Channel has access restrictions so that you can't enter it"),
tr("Channel has access restrictions but you can enter nonetheless"));
}
break;
}
return QVariant();
}
QVariant UserModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation != Qt::Horizontal)
return QVariant();
switch (role) {
case Qt::DisplayRole:
switch (section) {
case 0:
return tr("Name");
case 1:
return tr("Flags");
}
}
return QVariant();
}
void UserModel::recursiveClone(const ModelItem *old, ModelItem *item, QModelIndexList &from, QModelIndexList &to) {
if (old->qlChildren.isEmpty())
return;
beginInsertRows(index(item), 0, old->qlChildren.count());
for (int i = 0; i < old->qlChildren.count(); ++i) {
ModelItem *o = old->qlChildren.at(i);
ModelItem *mi = new ModelItem(o);
mi->parent = item;
item->qlChildren << mi;
from << createIndex(i, 0, o);
from << createIndex(i, 1, o);
to << createIndex(i, 0, mi);
to << createIndex(i, 1, mi);
}
endInsertRows();
for (int i = 0; i < old->qlChildren.count(); ++i)
recursiveClone(old->qlChildren.at(i), item->qlChildren.at(i), from, to);
}
ModelItem *UserModel::moveItem(ModelItem *oldparent, ModelItem *newparent, ModelItem *oldItem) {
// Here's the idea. We insert the item, update persistent indexes, THEN remove it.
// Get the current position of the item under its parent (aka its "row")
int oldrow = oldparent->qlChildren.indexOf(oldItem);
// Get the row of the item at its new position. This depends on whether we're moving a
// channel or a user.
int newrow = -1;
if (oldItem->cChan) {
newrow = newparent->insertIndex(oldItem->cChan);
} else {
newrow = newparent->insertIndex(oldItem->pUser);
}
if ((oldparent == newparent) && (newrow == oldrow)) {
// This is a no-op. We still claim that the data has changed in order
// to trigger potential event handlers.
emit dataChanged(index(oldItem), index(oldItem));
return oldItem;
}
// Shallow clone. newItem is the new ModelItem that will be added to newparent
ModelItem *newItem = new ModelItem(oldItem);
// Store the index if it's "active".
// The selection is stored as "from"-"to" pairs, so if we move up in the same channel,
// we'd move only "from" and select half the channel.
// Check whether the moved item is currently selected and if so, store it as a persistent
// model index in active. Also clear the selection as we're going to mess with the active
// item.
QTreeView *v = Global::get().mw->qtvUsers;
QItemSelectionModel *sel = v->selectionModel();
QPersistentModelIndex active;
QModelIndex oindex = createIndex(oldrow, 0, oldItem);
if (sel->isSelected(oindex) || (oindex == v->currentIndex())) {
active = index(oldItem);
v->clearSelection();
v->setCurrentIndex(QModelIndex());
}
// Check whether the oldItem is currently expanded in order to restore the same
// state once we have moved it.
bool expanded = v->isExpanded(index(oldItem));
if (newparent == oldparent) {
// If the moving happens within the same parent, we have to watch out that we use the correct
// row indices for our operation here.
// As we're inserting the new item before remving the old one, newrow has to be the index
// applicable before the removal (aka "as is" atm) whereas oldrow has to be applicable
// after we've inserted the new item.
if (oldrow >= newrow) {
// The new item will be inserted above the old one. Thus we have to account for that extra
// item in the used row index.
oldrow++;
} else {
newrow++;
}
}
// Insert the new item to its (new) parent
beginInsertRows(index(newparent), newrow, newrow);
newItem->parent = newparent;
newparent->qlChildren.insert(newrow, newItem);
if (oldItem->cChan) {
// When moving a channel, we'll also have to move any sub-channels
oldparent->cChan->removeChannel(oldItem->cChan);
newparent->cChan->addChannel(oldItem->cChan);
} else {
newparent->cChan->addClientUser(oldItem->pUser);
}
endInsertRows();
QModelIndexList from, to;
from << createIndex(oldrow, 0, oldItem);
from << createIndex(oldrow, 1, oldItem);
to << createIndex(newrow, 0, newItem);
to << createIndex(newrow, 1, newItem);
// Clone all children of oldItem and attach them to newItem
recursiveClone(oldItem, newItem, from, to);
// Update all persistent model indices that are affected by our action here. This includes (but is in general
// not limited to) the "active" index we potentially created above.
changePersistentIndexList(from, to);
// Now that we have added the new index, it is time to actually remove the old one
beginRemoveRows(index(oldparent), oldrow, oldrow);
oldparent->qlChildren.removeAt(oldrow);
endRemoveRows();
// oldItem is now longer needed as it is not present in the model anymore and all potential
// references to it should be updated to point to the new (moved) item instead.
// Thus we can delete it and all its children (which have been cloned and reference-updated
// as well.
oldItem->wipe();
delete oldItem;
if (active.isValid()) {
// If the moved item has been previously selected, we restore that selection to now be the
// new item using the "active" model index which has been updated to now point to the new
// item.
sel->select(active, QItemSelectionModel::SelectCurrent);
v->setCurrentIndex(active);
}
if (expanded) {
// If the old item (or rather the parent it has been living in) has been expanded,
// restore that state for the new item.
v->expand(index(newItem));
}
return newItem;
}
void UserModel::expandAll(Channel *c) {
QStack< Channel * > chans;
while (c) {
chans.push(c);
c = c->cParent;
}
while (!chans.isEmpty()) {
c = chans.pop();
Global::get().mw->qtvUsers->setExpanded(index(c), true);
}
}
void UserModel::collapseEmpty(Channel *c) {
while (c) {
ModelItem *mi = ModelItem::c_qhChannels.value(c);
if (mi->iUsers == 0)
Global::get().mw->qtvUsers->setExpanded(index(c), false);
else
break;
c = c->cParent;
}
}
void UserModel::ensureSelfVisible() {
if (!Global::get().uiSession)
return;
Global::get().mw->qtvUsers->scrollTo(index(ClientUser::get(Global::get().uiSession)));
}
void UserModel::recheckLinks() {
if (!Global::get().uiSession)
return;
ClientUser *clientUser = ClientUser::get(Global::get().uiSession);
if (!clientUser)
return;
bool bChanged = false;
Channel *home = clientUser->cChannel;
QSet< Channel * > all = home->allLinks();
if (all == qsLinked)
return;
QSet< Channel * > changed = (all - qsLinked);
changed += (qsLinked - all);
if ((all.count() == 1) || (qsLinked.count() == 1))
changed += home;
qsLinked = all;
foreach (Channel *c, changed) {
QModelIndex idx = index(c);
emit dataChanged(idx, idx);
bChanged = true;
}
if (bChanged)
updateOverlay();
}
ClientUser *UserModel::addUser(unsigned int id, const QString &name) {
ClientUser *p = ClientUser::add(id, this);
p->qsName = name;
ModelItem *item = new ModelItem(p);
connect(p, SIGNAL(talkingStateChanged()), this, SLOT(userStateChanged()));
connect(p, SIGNAL(muteDeafStateChanged()), this, SLOT(userStateChanged()));
connect(p, SIGNAL(prioritySpeakerStateChanged()), this, SLOT(userStateChanged()));
connect(p, SIGNAL(recordingStateChanged()), this, SLOT(userStateChanged()));
connect(p, &ClientUser::localVolumeAdjustmentsChanged, this, &UserModel::userStateChanged);
connect(p, &ClientUser::localNicknameChanged, this, &UserModel::userStateChanged);
Channel *c = Channel::get(Channel::ROOT_ID);
ModelItem *citem = ModelItem::c_qhChannels.value(c);
item->parent = citem;
int row = citem->insertIndex(p);
beginInsertRows(index(citem), row, row);
citem->qlChildren.insert(row, item);
c->addClientUser(p);
endInsertRows();
while (citem) {
citem->iUsers++;
citem = citem->parent;
}
updateOverlay();
emit userAdded(p->uiSession);
return p;
}
void UserModel::removeUser(ClientUser *p) {
// First remove all listener proxies this user has at the moment
removeChannelListener(p);
if (Global::get().uiSession && p->uiSession == Global::get().uiSession)
Global::get().uiSession = 0;
Channel *c = p->cChannel;
ModelItem *item = ModelItem::c_qhUsers.value(p);
ModelItem *citem = ModelItem::c_qhChannels.value(c);
int row = citem->qlChildren.indexOf(item);
beginRemoveRows(index(citem), row, row);
c->removeUser(p);
citem->qlChildren.removeAt(row);
endRemoveRows();
p->cChannel = nullptr;
ClientUser::remove(p);
qmHashes.remove(p->qsHash);
while (citem) {
citem->iUsers--;
citem = citem->parent;
}
if (Global::get().s.ceExpand == Settings::ChannelsWithUsers)
collapseEmpty(c);
updateOverlay();
emit userRemoved(p->uiSession);
delete p;
delete item;
}
void UserModel::moveUser(ClientUser *p, Channel *np) {
Channel *oc = p->cChannel;
ModelItem *opi = ModelItem::c_qhChannels.value(oc);
ModelItem *pi = ModelItem::c_qhChannels.value(np);
ModelItem *item = ModelItem::c_qhUsers.value(p);
item = moveItem(opi, pi, item);
if (p->uiSession == Global::get().uiSession) {
ensureSelfVisible();
recheckLinks();
}
while (opi) {
opi->iUsers--;
opi = opi->parent;
}
while (pi) {
pi->iUsers++;
pi = pi->parent;
}
if (Global::get().s.ceExpand == Settings::ChannelsWithUsers) {
expandAll(np);
collapseEmpty(oc);
}
updateOverlay();
}
void UserModel::renameUser(ClientUser *p, const QString &name) {
Channel *c = p->cChannel;
p->qsName = name;
ModelItem *pi = ModelItem::c_qhChannels.value(c);
ModelItem *item = ModelItem::c_qhUsers.value(p);
moveItem(pi, pi, item);
updateOverlay();
}
void UserModel::setUserId(ClientUser *p, int id) {
p->iId = id;
QModelIndex idx = index(p, 0);
emit dataChanged(idx, idx);
}
void UserModel::setHash(ClientUser *p, const QString &hash) {
if (!p->qsHash.isEmpty())
qmHashes.remove(p->qsHash);
p->qsHash = hash;
qmHashes.insert(p->qsHash, p);
}
void UserModel::setFriendName(ClientUser *p, const QString &name) {
p->qsFriendName = name;
QModelIndex idx = index(p, 0);
emit dataChanged(idx, idx);
}
void UserModel::setComment(ClientUser *cu, const QString &comment) {
cu->qbaCommentHash = comment.isEmpty() ? QByteArray() : sha1(comment);
if (comment != cu->qsComment) {
ModelItem *item = ModelItem::c_qhUsers.value(cu);
int oldstate = (cu->qsComment.isEmpty() && cu->qbaCommentHash.isEmpty()) ? 0 : (item->bCommentSeen ? 2 : 1);
int newstate = 0;
cu->qsComment = comment;
if (!comment.isEmpty()) {
Global::get().db->setBlob(cu->qbaCommentHash, cu->qsComment.toUtf8());
if (cu->uiSession == uiSessionComment) {
uiSessionComment = 0;
item->bCommentSeen = false;
if (bClicked) {
QRect r = Global::get().mw->qtvUsers->visualRect(index(cu));
QWhatsThis::showText(Global::get().mw->qtvUsers->viewport()->mapToGlobal(r.bottomRight()),
data(index(cu, 0), Qt::ToolTipRole).toString(), Global::get().mw->qtvUsers);
} else {
QToolTip::showText(QCursor::pos(), data(index(cu, 0), Qt::ToolTipRole).toString(),
Global::get().mw->qtvUsers);
}
} else if (cu->uiSession == ~uiSessionComment) {
uiSessionComment = 0;
if (cu->uiSession == Global::get().uiSession) {
QTimer::singleShot(0, Global::get().mw, SLOT(on_qaSelfComment_triggered()));
} else {
Global::get().mw->cuContextUser = cu;
QTimer::singleShot(0, Global::get().mw, SLOT(on_qaUserCommentView_triggered()));
}
} else {
item->bCommentSeen = Global::get().db->seenComment(item->hash(), cu->qbaCommentHash);
newstate = item->bCommentSeen ? 2 : 1;
}
} else {
item->bCommentSeen = true;
}
if (oldstate != newstate) {
QModelIndex idx = index(cu, 0);
emit dataChanged(idx, idx);
}
}
}
void UserModel::setCommentHash(ClientUser *cu, const QByteArray &hash) {
if (hash != cu->qbaCommentHash) {
ModelItem *item = ModelItem::c_qhUsers.value(cu);
int oldstate = (cu->qsComment.isEmpty() && cu->qbaCommentHash.isEmpty()) ? 0 : (item->bCommentSeen ? 2 : 1);
int newstate;
cu->qsComment = QString();
cu->qbaCommentHash = hash;
item->bCommentSeen = Global::get().db->seenComment(item->hash(), cu->qbaCommentHash);
newstate = item->bCommentSeen ? 2 : 1;
if (oldstate != newstate) {
QModelIndex idx = index(cu, 0);
emit dataChanged(idx, idx);
}
}
}
void UserModel::setComment(Channel *c, const QString &comment) {
c->qbaDescHash = comment.isEmpty() ? QByteArray() : sha1(comment);
if (comment != c->qsDesc) {
ModelItem *item = ModelItem::c_qhChannels.value(c);
int oldstate = c->qsDesc.isEmpty() ? 0 : (item->bCommentSeen ? 2 : 1);
int newstate = 0;
c->qsDesc = comment;
if (!comment.isEmpty()) {
Global::get().db->setBlob(c->qbaDescHash, c->qsDesc.toUtf8());
if (c->iId == static_cast< unsigned int >(iChannelDescription)) {
iChannelDescription = -1;
item->bCommentSeen = false;
if (bClicked) {
QRect r = Global::get().mw->qtvUsers->visualRect(index(c));
QWhatsThis::showText(Global::get().mw->qtvUsers->viewport()->mapToGlobal(r.bottomRight()),
data(index(c, 0), Qt::ToolTipRole).toString(), Global::get().mw->qtvUsers);
} else {
QToolTip::showText(QCursor::pos(), data(index(c, 0), Qt::ToolTipRole).toString(),
Global::get().mw->qtvUsers);
}
} else {
item->bCommentSeen = Global::get().db->seenComment(item->hash(), c->qbaDescHash);
newstate = item->bCommentSeen ? 2 : 1;
}
} else {
item->bCommentSeen = true;
}
if (oldstate != newstate) {
QModelIndex idx = index(c, 0);
emit dataChanged(idx, idx);
}
}
}
void UserModel::setCommentHash(Channel *c, const QByteArray &hash) {
if (hash != c->qbaDescHash) {
ModelItem *item = ModelItem::c_qhChannels.value(c);
int oldstate = (c->qsDesc.isEmpty() && c->qbaDescHash.isEmpty()) ? 0 : (item->bCommentSeen ? 2 : 1);
int newstate;
c->qsDesc = QString();
c->qbaDescHash = hash;
item->bCommentSeen = Global::get().db->seenComment(item->hash(), hash);
newstate = item->bCommentSeen ? 2 : 1;
if (oldstate != newstate) {
QModelIndex idx = index(c, 0);
emit dataChanged(idx, idx);
}
}
}
void UserModel::seenComment(const QModelIndex &idx) {
ModelItem *item;
item = static_cast< ModelItem * >(idx.internalPointer());
if (item->bCommentSeen)
return;
item->bCommentSeen = true;
emit dataChanged(idx, idx);
if (item->pUser)
Global::get().db->setSeenComment(item->hash(), item->pUser->qbaCommentHash);
else
Global::get().db->setSeenComment(item->hash(), item->cChan->qbaDescHash);
}
void UserModel::renameChannel(Channel *c, const QString &name) {
c->qsName = name;
if (c->iId == 0) {
QModelIndex idx = index(c);
emit dataChanged(idx, idx);
} else {
Channel *pc = c->cParent;
ModelItem *pi = ModelItem::c_qhChannels.value(pc);
ModelItem *item = ModelItem::c_qhChannels.value(c);
moveItem(pi, pi, item);
}
emit channelRenamed(c->iId);
}
void UserModel::repositionChannel(Channel *c, const int position) {
c->iPosition = position;
if (c->iId == 0) {
QModelIndex idx = index(c);
emit dataChanged(idx, idx);
} else {
Channel *pc = c->cParent;
ModelItem *pi = ModelItem::c_qhChannels.value(pc);
ModelItem *item = ModelItem::c_qhChannels.value(c);
moveItem(pi, pi, item);
}
}
Channel *UserModel::addChannel(unsigned int id, Channel *p, const QString &name) {
Channel *c = Channel::add(id, name);
if (!c)
return nullptr;
ModelItem *item = new ModelItem(c);
ModelItem *citem = ModelItem::c_qhChannels.value(p);
item->parent = citem;
int row = citem->insertIndex(c);
beginInsertRows(index(citem), row, row);
p->addChannel(c);
citem->qlChildren.insert(row, item);
endInsertRows();
if (Global::get().s.ceExpand == Settings::AllChannels)
Global::get().mw->qtvUsers->setExpanded(index(item), true);
emit channelAdded(c->iId);
return c;
}
void UserModel::addChannelListener(ClientUser *p, Channel *c) {
ModelItem *item = new ModelItem(p, true);
ModelItem *citem = ModelItem::c_qhChannels.value(c);
item->parent = citem;
int row = citem->insertIndex(p, true);
beginInsertRows(index(citem), row, row);
citem->qlChildren.insert(row, item);
endInsertRows();
while (citem) {
citem->iUsers++;
citem = citem->parent;
}
updateOverlay();
}
void UserModel::removeChannelListener(const ClientUser *p, const Channel *c) {
// The way operator[] works for a QHash is that it'll insert a default-constructed
// object first, before returning a reference to it, in case there is no entry for
// the provided key yet. Thus we never have to worry about explicitly adding an empty
// list for a new user before accessing it.
const QList< ModelItem * > &items = ModelItem::s_userProxies[p];
if (items.isEmpty()) {
return;
}
if (c) {
ModelItem *citem = ModelItem::c_qhChannels.value(c);
ModelItem *item = nullptr;
for (int i = 0; i < items.size(); i++) {
if (citem->qlChildren.contains(items[i])) {
item = items[i];
break;
}
}
if (item) {
removeChannelListener(item, citem);
} else {
qCritical("UserModel::removeChannelListener: Can't find item for provided channel");
}
} else {
// remove all items
foreach (ModelItem *currentItem, items) { removeChannelListener(currentItem); }
}
}
bool UserModel::isChannelListener(const QModelIndex &idx) const {
if (!idx.isValid()) {
return false;
}
ModelItem *item;
item = static_cast< ModelItem * >(idx.internalPointer());
return item->isListener;
}
void UserModel::setSelectedChannelListener(unsigned int userSession, unsigned int channelID) {
QModelIndex idx = channelListenerIndex(ClientUser::get(userSession), Channel::get(channelID));
if (!idx.isValid()) {
return;
}
QTreeView *v = Global::get().mw->qtvUsers;
if (v) {
v->setCurrentIndex(idx);
}
}
void UserModel::removeChannelListener(ModelItem *item, ModelItem *citem) {
if (!citem) {
citem = item->parent;
}
if (!item || !citem) {
qCritical("UserModel::removeChannelListener: Invalid state encountered");
return;
}
if (!citem->qlChildren.contains(item)) {
qCritical("UserModel::removeChannelListener: Item does not match parent");
return;
}
ClientUser *p = item->pUser;
Channel *c = citem->cChan;
if (!p) {
qCritical("UserModel::removeChannelListener: Can't find associated ClientUser");
return;
}
if (!c) {
qCritical("UserModel::removeChannelListener: Can't find associated Channel");
return;
}
int row = citem->qlChildren.indexOf(item);
beginRemoveRows(index(citem), row, row);
citem->qlChildren.removeAt(row);
endRemoveRows();
while (citem) {
citem->iUsers--;
citem = citem->parent;
}
if (Global::get().s.ceExpand == Settings::ChannelsWithUsers)
collapseEmpty(c);
updateOverlay();
delete item;
}
bool UserModel::removeChannel(Channel *c, const bool onlyIfUnoccupied) {
const ModelItem *item = ModelItem::c_qhChannels.value(c);
if (onlyIfUnoccupied && item->iUsers != 0)
return false; // Checks full hierarchy
foreach (const ModelItem *i, item->qlChildren) {
if (i->pUser) {
if (i->isListener) {
removeChannelListener(i->pUser, c);
} else {
removeUser(i->pUser);
}
} else {
removeChannel(i->cChan);
}
}
Channel *p = c->cParent;
if (!p)
return true;
ModelItem *citem = ModelItem::c_qhChannels.value(p);
int row = citem->rowOf(c);
beginRemoveRows(index(citem), row, row);
p->removeChannel(c);
citem->qlChildren.removeAt(row);
qsLinked.remove(c);
endRemoveRows();
Channel::remove(c);
emit channelRemoved(c->iId);
delete item;
delete c;
return true;
}
void UserModel::moveChannel(Channel *c, Channel *p) {
Channel *oc = c->cParent;
ModelItem *opi = ModelItem::c_qhChannels.value(c->cParent);
ModelItem *pi = ModelItem::c_qhChannels.value(p);
ModelItem *item = ModelItem::c_qhChannels.value(c);
item = moveItem(opi, pi, item);
while (opi) {
opi->iUsers -= item->iUsers;
opi = opi->parent;
}
while (pi) {
pi->iUsers += item->iUsers;
pi = pi->parent;
}
ensureSelfVisible();
if (Global::get().s.ceExpand == Settings::ChannelsWithUsers) {
collapseEmpty(oc);
}
}
void UserModel::linkChannels(Channel *c, QList< Channel * > links) {
foreach (Channel *l, links)
c->link(l);
recheckLinks();
}
void UserModel::unlinkChannels(Channel *c, QList< Channel * > links) {
foreach (Channel *l, links)
c->unlink(l);
recheckLinks();
}
void UserModel::unlinkAll(Channel *c) {
c->unlink(nullptr);
recheckLinks();
}
void UserModel::removeAll() {
ModelItem *item = miRoot;
ModelItem *i;
uiSessionComment = 0;
iChannelDescription = -1;
bClicked = false;
// in order to avoid complications, we remove all ChannelListeners first
foreach (i, item->qlChildren) {
if (i->pUser && i->isListener) {
removeChannelListener(i, item);
}
}
foreach (i, item->qlChildren) {
if (i->pUser)
removeUser(i->pUser);
else
removeChannel(i->cChan);
}
qsLinked.clear();
updateOverlay();
}
ClientUser *UserModel::getUser(const QModelIndex &idx) const {
if (!idx.isValid())
return nullptr;
ModelItem *item;
item = static_cast< ModelItem * >(idx.internalPointer());
return item->pUser;
}
ClientUser *UserModel::getUser(const QString &hash) const {
return qmHashes.value(hash);
}
ClientUser *UserModel::getSelectedUser() const {
QModelIndex selected = getSelectedIndex();
if (selected.isValid()) {
ModelItem *item = static_cast< ModelItem * >(selected.internalPointer());
return item->pUser;
}
return nullptr;
}
void UserModel::setSelectedUser(unsigned int session) {
QModelIndex idx = index(ClientUser::get(session));
if (!idx.isValid()) {
return;
}
QTreeView *v = Global::get().mw->qtvUsers;
if (v) {
v->setCurrentIndex(idx);
}
}
Channel *UserModel::getChannel(const QModelIndex &idx) const {
if (!idx.isValid())
return nullptr;
ModelItem *item;
item = static_cast< ModelItem * >(idx.internalPointer());
if (item->pUser)
if (item->parent && item->parent->cChan) {
return item->parent->cChan;
} else {
// Failsafe in case the item does not have a parent
qWarning("UserModel::getChannel encountered weird program flow - that's a bug (please report)!");
return item->pUser->cChannel;
}
else
return item->cChan;
}
Channel *UserModel::getSelectedChannel() const {
QModelIndex selected = getSelectedIndex();
if (selected.isValid()) {
ModelItem *item = static_cast< ModelItem * >(selected.internalPointer());
return item->cChan;
}
return nullptr;
}
void UserModel::setSelectedChannel(unsigned int id) {
QModelIndex idx = index(Channel::get(id));
if (!idx.isValid()) {
return;
}
QTreeView *v = Global::get().mw->qtvUsers;
if (v) {
v->setCurrentIndex(idx);
}
}
Channel *UserModel::getSubChannel(Channel *p, int idx) const {
ModelItem *item = ModelItem::c_qhChannels.value(p);
if (!item)
return nullptr;
foreach (ModelItem *i, item->qlChildren) {
if (i->cChan) {
if (idx == 0)
return i->cChan;
idx--;
}
}
return nullptr;
}
void UserModel::userStateChanged() {
ClientUser *user = qobject_cast< ClientUser * >(sender());
if (!user)
return;
const QModelIndex idx = index(user);
emit dataChanged(idx, idx);
updateOverlay();
}
void UserModel::on_channelListenerLocalVolumeAdjustmentChanged(unsigned int channelID, float oldValue, float newValue) {
Q_UNUSED(oldValue);
Q_UNUSED(newValue);
const QModelIndex idx = channelListenerIndex(ClientUser::get(Global::get().uiSession), Channel::get(channelID));
emit dataChanged(idx, idx);
}
void UserModel::forceVisualUpdate(Channel *c) {
QModelIndex idx;
if (c) {
idx = index(c);
}
emit dataChanged(idx, idx);
updateOverlay();
}
Qt::DropActions UserModel::supportedDropActions() const {
return Qt::MoveAction;
}
QStringList UserModel::mimeTypes() const {
QStringList sl;
sl << QLatin1String("mumble/dragentry");
return sl;
}
QMimeData *UserModel::mimeData(const QModelIndexList &idxs) const {
QModelIndex idx;
QByteArray qba;
QDataStream ds(&qba, QIODevice::WriteOnly);
foreach (idx, idxs) {
ClientUser *p = getUser(idx);
Channel *c = getChannel(idx);
if (p) {
ds << false;
ds << p->uiSession;
} else if (c) {
ds << true;
ds << c->iId;
}
}
QMimeData *md = new QMimeData();
md->setData(QLatin1String("mumble/dragentry"), qba);
return md;
}
bool UserModel::dropMimeData(const QMimeData *md, Qt::DropAction, int row, int column, const QModelIndex &p) {
#define NAMECMPCHANNEL(first, second) (QString::localeAwareCompare(first->qsName, second->qsName) > 0)
if (!md->hasFormat(mimeTypes().at(0)))
return false;
QByteArray qba = md->data(mimeTypes().at(0));
QDataStream ds(qba);
bool isChannel;
int iId = -1;
unsigned int uiSession = 0;
ds >> isChannel;
if (isChannel)
ds >> iId;
else
ds >> uiSession;
Channel *c;
if (!p.isValid()) {
c = Channel::get(Channel::ROOT_ID);
} else {
c = getChannel(p);
}
if (!c)
return false;
expandAll(c);
if (!isChannel) {
// User dropped somewhere
int ret;
switch (Global::get().s.ceUserDrag) {
case Settings::Ask:
ret = QMessageBox::question(Global::get().mw, QLatin1String("Mumble"),
tr("Are you sure you want to drag this user?"), QMessageBox::Yes,
QMessageBox::No);
if (ret == QMessageBox::No)
return false;
break;
case Settings::DoNothing:
Global::get().l->log(
Log::Information,
MainWindow::tr("You have User Dragging set to \"Do Nothing\" so the user wasn't moved."));
return false;
break;
case Settings::Move:
break;
}
MumbleProto::UserState mpus;
mpus.set_session(uiSession);
mpus.set_channel_id(c->iId);
Global::get().sh->sendMessage(mpus);
} else if (c->iId != static_cast< unsigned int >(iId)) {
// Channel dropped somewhere (not on itself)
int ret;
switch (Global::get().s.ceChannelDrag) {
case Settings::Ask:
ret = QMessageBox::question(Global::get().mw, QLatin1String("Mumble"),
tr("Are you sure you want to drag this channel?"), QMessageBox::Yes,
QMessageBox::No);
if (ret == QMessageBox::No)
return false;
break;
case Settings::DoNothing:
Global::get().l->log(
Log::Information,
MainWindow::tr("You have Channel Dragging set to \"Do Nothing\" so the channel wasn't moved."));
return false;
break;
case Settings::Move:
break;
default:
Global::get().l->log(Log::CriticalError,
MainWindow::tr("Unknown Channel Drag mode in UserModel::dropMimeData."));
return false;
break;
}
long long inewpos = 0;
Channel *dropped = Channel::c_qhChannels.value(static_cast< unsigned int >(iId));
if (!dropped)
return false;
if (p.isValid()) {
ModelItem *pi = static_cast< ModelItem * >(p.internalPointer());
if (pi->pUser)
pi = pi->parent;
int ifirst = 0;
int ilast = pi->rows() - 1;
if (ilast > 0) {
while (pi->userAt(ifirst) && ifirst < ilast)
ifirst++;
while (pi->userAt(ilast) && ilast > 0)
ilast--;
}
if (row == -1 && column == -1) {
// Dropped on item
if (getUser(p)) {
// Dropped on player
if (ilast > 0) {
if (pi->bUsersTop) {
if (pi->channelAt(ifirst) == dropped || NAMECMPCHANNEL(pi->channelAt(ifirst), dropped)) {
if (dropped->iPosition == pi->channelAt(ifirst)->iPosition)
return true;
inewpos = pi->channelAt(ifirst)->iPosition;
} else {
inewpos = static_cast< long long >(pi->channelAt(ifirst)->iPosition) - 20;
}
} else {
if (dropped == pi->channelAt(ilast) || NAMECMPCHANNEL(dropped, pi->channelAt(ilast))) {
if (pi->channelAt(ilast)->iPosition == dropped->iPosition)
return true;
inewpos = pi->channelAt(ilast)->iPosition;
} else {
inewpos = static_cast< long long >(pi->channelAt(ilast)->iPosition) + 20;
}
}
}
}
} else {
// Dropped between items
if (ilast == 0) {
// No channels in there yet
} else if (row <= ifirst) {
if (pi->channelAt(ifirst) == dropped || NAMECMPCHANNEL(pi->channelAt(ifirst), dropped)) {
if (dropped->iPosition == pi->channelAt(ifirst)->iPosition)
return true;
inewpos = pi->channelAt(ifirst)->iPosition;
} else {
inewpos = static_cast< long long >(pi->channelAt(ifirst)->iPosition) - 20;
}
} else if (row > ilast) {
if (dropped == pi->channelAt(ilast) || NAMECMPCHANNEL(dropped, pi->channelAt(ilast))) {
if (pi->channelAt(ilast)->iPosition == dropped->iPosition)
return true;
inewpos = pi->channelAt(ilast)->iPosition;
} else {
inewpos = static_cast< long long >(pi->channelAt(ilast)->iPosition) + 20;
}
} else {
// Dropped between channels
Channel *lower = pi->channelAt(row);
Channel *upper = pi->channelAt(row - 1);
if (lower->iPosition == upper->iPosition && NAMECMPCHANNEL(lower, dropped)
&& NAMECMPCHANNEL(dropped, upper)) {
inewpos = upper->iPosition;
} else if (lower->iPosition > upper->iPosition && NAMECMPCHANNEL(lower, dropped)) {
inewpos = lower->iPosition;
} else if (lower->iPosition > upper->iPosition && NAMECMPCHANNEL(dropped, upper)) {
inewpos = upper->iPosition;
} else if (lower == dropped || upper == dropped) {
return true;
} else if (abs(lower->iPosition) - abs(upper->iPosition) > 1) {
inewpos = upper->iPosition + (abs(lower->iPosition) - abs(upper->iPosition)) / 2;
} else {
// Not enough space, other channels have to be moved
if (static_cast< long long >(pi->channelAt(ilast)->iPosition) + 40 > INT_MAX) {
QMessageBox::critical(Global::get().mw, QLatin1String("Mumble"),
tr("Cannot perform this movement automatically, please reset the "
"numeric sorting indicators or adjust it manually."));
return false;
}
for (int i = row; i <= ilast; i++) {
Channel *tmp = pi->channelAt(i);
if (tmp != dropped) {
MumbleProto::ChannelState mpcs;
mpcs.set_channel_id(tmp->iId);
mpcs.set_position(tmp->iPosition + 40);
Global::get().sh->sendMessage(mpcs);
}
}
inewpos = upper->iPosition + 20;
}
}
}
}
if (inewpos > INT_MAX || inewpos < INT_MIN) {
QMessageBox::critical(Global::get().mw, QLatin1String("Mumble"),
tr("Cannot perform this movement automatically, please reset the numeric sorting "
"indicators or adjust it manually."));
return false;
}
MumbleProto::ChannelState mpcs;
mpcs.set_channel_id(static_cast< unsigned int >(iId));
if (dropped->parent() != c)
mpcs.set_parent(c->iId);
mpcs.set_position(static_cast< int >(inewpos));
Global::get().sh->sendMessage(mpcs);
}
return true;
#undef NAMECMPCHANNEL
}
void UserModel::updateOverlay() const {
#ifdef USE_OVERLAY
Global::get().o->updateOverlay();
#endif
Global::get().lcd->updateUserView();
}
QString UserModel::createDisplayString(const ClientUser &user, bool isChannelListener, const Channel *parentChannel) {
// Get the configured volume adjustment. Depending on whether
// this display string is for a ChannelListener or a regular user, we have to fetch
// the volume adjustment differently.
float volumeAdjustment = 1.0f;
if (isChannelListener) {
if (parentChannel && user.uiSession == Global::get().uiSession) {
// Only the listener of the local user can have a volume adjustment
volumeAdjustment =
Global::get()
.channelListenerManager->getListenerVolumeAdjustment(user.uiSession, parentChannel->iId)
.factor;
}
} else {
volumeAdjustment = user.getLocalVolumeAdjustments();
}
// Transform the adjustment into dB
int localVolumeDecibel = VolumeAdjustment::toIntegerDBAdjustment(volumeAdjustment);
// Create a friend-tag
QString friendTag;
if (!user.qsFriendName.isEmpty() && user.qsName.compare(user.qsFriendName, Qt::CaseInsensitive) != 0) {
friendTag = QString::fromLatin1("(%2)").arg(user.qsFriendName);
}
// Create a nickname-tag
QString nickname = user.getLocalNickname();
// Create a tag that indicates the volume adjustments
QString volumeTag;
if (std::abs(localVolumeDecibel) > 0 && Global::get().s.bShowVolumeAdjustments) {
volumeTag = QString::asprintf("|%+d|", localVolumeDecibel);
}
QString displayString;
if (!Global::get().s.bShowNicknamesOnly || nickname.isEmpty()) {
displayString += user.qsName;
} else {
displayString += nickname;
}
if (!friendTag.isEmpty()) {
displayString += " " + friendTag;
}
if (!Global::get().s.bShowNicknamesOnly && !nickname.isEmpty()
&& user.qsName.compare(nickname, Qt::CaseInsensitive) != 0) {
displayString += " " + QString::fromLatin1("[%1]").arg(nickname);
}
if (!volumeTag.isEmpty()) {
displayString += " " + volumeTag;
}
return displayString;
}