mumble-voip_mumble/src/mumble/UserListModel.cpp

302 lines
7.6 KiB
C++

// Copyright 2013-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 "UserListModel.h"
#include "Channel.h"
#include "QtUtils.h"
#include "Utils.h"
#include <algorithm>
#ifdef _MSC_VER
# include <functional>
#endif
#include <vector>
UserListModel::UserListModel(const MumbleProto::UserList &userList, QObject *parent_)
: QAbstractTableModel(parent_), m_legacyMode(false) {
m_userList.reserve(userList.users_size());
for (int i = 0; i < userList.users_size(); ++i) {
m_userList.append(userList.users(i));
}
if (!m_userList.empty()) {
const MumbleProto::UserList_User &user = m_userList.back();
m_legacyMode = !user.has_last_seen() || !user.has_last_channel();
}
}
int UserListModel::rowCount(const QModelIndex &parentIndex) const {
if (parentIndex.isValid())
return 0;
return m_userList.size();
}
int UserListModel::columnCount(const QModelIndex &parentIndex) const {
if (parentIndex.isValid())
return 0;
if (m_legacyMode) {
// Only COL_NICK
return 1;
}
return COUNT_COL;
}
QVariant UserListModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation != Qt::Horizontal)
return QVariant();
if (section < 0 || section >= columnCount())
return QVariant();
if (role == Qt::DisplayRole) {
switch (section) {
case COL_NICK:
return tr("Nick");
case COL_INACTIVEDAYS:
return tr("Inactive days");
case COL_LASTCHANNEL:
return tr("Last channel");
default:
return QVariant();
}
}
return QVariant();
}
QVariant UserListModel::data(const QModelIndex &dataIndex, int role) const {
if (!dataIndex.isValid())
return QVariant();
if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
return QVariant();
if (dataIndex.column() >= columnCount())
return QVariant();
const MumbleProto::UserList_User &user = m_userList[dataIndex.row()];
if (role == Qt::DisplayRole) {
switch (dataIndex.column()) {
case COL_NICK:
return u8(user.name());
case COL_INACTIVEDAYS:
return lastSeenToTodayDayCount(user.last_seen());
case COL_LASTCHANNEL:
return pathForChannelId(user.last_channel());
default:
return QVariant();
}
} else if (role == Qt::ToolTipRole) {
switch (dataIndex.column()) {
case COL_INACTIVEDAYS:
return tr("Last seen: %1")
.arg(user.last_seen().empty() ? tr("Never") : u8(user.last_seen()).toHtmlEscaped());
case COL_LASTCHANNEL:
return tr("Channel ID: %1").arg(user.last_channel());
default:
return QVariant();
}
} else if (role == Qt::UserRole) {
switch (dataIndex.column()) {
case COL_INACTIVEDAYS:
return isoUTCToDateTime(user.last_seen());
case COL_LASTCHANNEL:
return user.last_channel();
default:
return QVariant();
}
} else if (role == Qt::EditRole) {
if (dataIndex.column() == COL_NICK) {
return u8(user.name());
}
}
return QVariant();
}
bool UserListModel::setData(const QModelIndex &dataIndex, const QVariant &value, int role) {
if (!dataIndex.isValid())
return false;
if (dataIndex.column() != COL_NICK || role != Qt::EditRole)
return false;
if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
return false;
const std::string newNick = u8(value.toString());
if (newNick.empty()) {
// Empty nick is not valid
return false;
}
MumbleProto::UserList_User &user = m_userList[dataIndex.row()];
if (newNick != user.name()) {
foreach (const MumbleProto::UserList_User &otherUser, m_userList) {
if (otherUser.name() == newNick) {
// Duplicate is not valid
return false;
}
}
user.set_name(newNick);
m_changes[user.user_id()] = user;
emit dataChanged(dataIndex, dataIndex);
}
return true;
}
Qt::ItemFlags UserListModel::flags(const QModelIndex &flagIndex) const {
const Qt::ItemFlags original = QAbstractTableModel::flags(flagIndex);
if (flagIndex.column() == COL_NICK) {
return original | Qt::ItemIsEditable;
}
return original;
}
bool UserListModel::removeRows(int row, int count, const QModelIndex &parentIndex) {
if (row + count > m_userList.size())
return false;
beginRemoveRows(parentIndex, row, row + count - 1);
ModelUserList::Iterator startIt = m_userList.begin() + row;
ModelUserList::Iterator endIt = startIt + count;
for (ModelUserList::Iterator it = startIt; it != endIt; ++it) {
it->clear_name();
m_changes[it->user_id()] = *it;
}
m_userList.erase(startIt, endIt);
endRemoveRows();
return true;
}
void UserListModel::removeRowsInSelection(const QItemSelection &selection) {
QModelIndexList indices = selection.indexes();
std::vector< int > rows;
rows.reserve(static_cast< std::size_t >(indices.size()));
foreach (const QModelIndex &idx, indices) {
if (idx.column() != COL_NICK)
continue;
rows.push_back(idx.row());
}
// Make sure to presort the rows so we work from back (high) to front (low).
// This prevents the row numbers from becoming invalid after removing a group.
// The basic idea is to take a number of sorted rows (e.g. 10,9,5,3,2,1) and
// delete them with the minimum number of removeRows calls. This means grouping
// adjacent rows (e.g. (10,9),(5),(3,2,1)) and using a removeRows call for each group.
std::sort(rows.begin(), rows.end(), std::greater< int >());
int nextRow = -2;
int groupRowCount = 0;
for (size_t i = 0; i < rows.size(); ++i) {
if (rows[i] == nextRow) {
++groupRowCount;
--nextRow;
} else {
if (groupRowCount > 0) {
// Remove previous group
const int lastRowInGroup = nextRow + 1;
removeRows(lastRowInGroup, groupRowCount);
}
// Start next group
nextRow = rows[i] - 1;
groupRowCount = 1;
}
}
if (groupRowCount > 0) {
// Remove leftover group
const int lastRowInGroup = nextRow + 1;
removeRows(lastRowInGroup, groupRowCount);
}
}
MumbleProto::UserList UserListModel::getUserListUpdate() const {
MumbleProto::UserList updateList;
for (ModelUserListChangeMap::ConstIterator it = m_changes.constBegin(); it != m_changes.constEnd(); ++it) {
MumbleProto::UserList_User *user = updateList.add_users();
*user = it.value();
}
return updateList;
}
bool UserListModel::isUserListDirty() const {
return !m_changes.empty();
}
bool UserListModel::isLegacy() const {
return m_legacyMode;
}
QVariant UserListModel::lastSeenToTodayDayCount(const std::string &lastSeenDate) const {
if (lastSeenDate.empty())
return QVariant();
QVariant count = m_stringToLastSeenToTodayCount.value(u8(lastSeenDate));
if (count.isNull()) {
QDateTime dt = isoUTCToDateTime(lastSeenDate);
if (!dt.isValid()) {
// Not convertible to int
return QVariant();
}
count = dt.daysTo(QDateTime::currentDateTime().toUTC());
m_stringToLastSeenToTodayCount.insert(u8(lastSeenDate), count);
}
return count;
}
QString UserListModel::pathForChannelId(const unsigned int channelId) const {
QString path = m_channelIdToPathMap.value(channelId);
if (path.isNull()) {
path = QLatin1String("-");
Channel *channel = Channel::get(channelId);
if (channel) {
QStringList pathParts;
while (channel->cParent) {
pathParts.prepend(channel->qsName);
channel = channel->cParent;
}
pathParts.append(QString());
path = QLatin1String("/ ") + pathParts.join(QLatin1String(" / "));
}
m_channelIdToPathMap.insert(channelId, path);
}
return path;
}
QDateTime UserListModel::isoUTCToDateTime(const std::string &isoTime) const {
QDateTime dt = QDateTime::fromString(u8(isoTime), Qt::ISODate);
dt.setTimeSpec(Qt::UTC);
return dt;
}