mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-29 09:22:18 -07:00
[UserList] Unify friends/online/ignored list with section dividers and add search bar.
Took 31 minutes Took 7 seconds
This commit is contained in:
parent
daa896866f
commit
46d3dba6b4
10 changed files with 692 additions and 69 deletions
|
|
@ -260,6 +260,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/server/user/user_info_connection.cpp
|
||||
src/interface/widgets/server/user/user_list_manager.cpp
|
||||
src/interface/widgets/server/user/user_list_painter.cpp
|
||||
src/interface/widgets/server/user/user_list_panel_widget.cpp
|
||||
src/interface/widgets/server/user/user_list_widget.cpp
|
||||
src/interface/widgets/settings_page/abstract_settings_page.cpp
|
||||
src/interface/widgets/settings_page/appearance_settings_page.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
#include "user_list_panel_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "user_list_manager.h"
|
||||
#include "user_list_widget.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mainLayout->setSpacing(2);
|
||||
|
||||
searchBar = new QLineEdit(this);
|
||||
searchBar->setClearButtonEnabled(true);
|
||||
mainLayout->addWidget(searchBar);
|
||||
|
||||
userList = new UserListWidget(_tabSupervisor, _client, UserListWidget::RoomList, this);
|
||||
userList->setSectioned({QStringLiteral("buddy"), QStringLiteral("online"), QStringLiteral("ignore")});
|
||||
mainLayout->addWidget(userList, 1);
|
||||
|
||||
connect(searchBar, &QLineEdit::textChanged, userList, &UserListWidget::setFilterText);
|
||||
|
||||
connect(userList, &UserListWidget::sectionExpanded, this, &UserListPanelWidget::persistExpandedSections);
|
||||
connect(userList, &UserListWidget::openMessageDialog, this, &UserListPanelWidget::openMessageDialog);
|
||||
|
||||
// Restore the persisted expansion state, then apply it to the tree.
|
||||
const QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
|
||||
for (const QString §ionId : userList->getSectionIds()) {
|
||||
userList->setSectionExpanded(sectionId, expandedSections.contains(sectionId));
|
||||
}
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void UserListPanelWidget::bind(UserListManager *manager)
|
||||
{
|
||||
userList->bind(manager);
|
||||
}
|
||||
|
||||
void UserListPanelWidget::persistExpandedSections(const QString §ionId, bool expanded)
|
||||
{
|
||||
QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
|
||||
if (expanded) {
|
||||
if (!expandedSections.contains(sectionId)) {
|
||||
expandedSections.append(sectionId);
|
||||
}
|
||||
} else {
|
||||
expandedSections.removeAll(sectionId);
|
||||
}
|
||||
SettingsCache::instance().userInterface().setUserListExpandedSections(expandedSections);
|
||||
}
|
||||
|
||||
void UserListPanelWidget::retranslateUi()
|
||||
{
|
||||
searchBar->setPlaceholderText(tr("Search users..."));
|
||||
userList->retranslateUi();
|
||||
}
|
||||
|
||||
UserListWidget *UserListPanelWidget::getUserList() const
|
||||
{
|
||||
return userList;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* @file user_list_panel_widget.h
|
||||
* @ingroup Lobby
|
||||
*/
|
||||
|
||||
#ifndef COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
#define COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class AbstractClient;
|
||||
class QLineEdit;
|
||||
class TabSupervisor;
|
||||
class UserListManager;
|
||||
class UserListWidget;
|
||||
|
||||
/**
|
||||
* A unified user list: a search bar above a single tree whose section headers
|
||||
* (buddy, online, ignored) are inline dividers. The tree owns the scrolling.
|
||||
*/
|
||||
class UserListPanelWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserListPanelWidget(TabSupervisor *tabSupervisor, AbstractClient *client, QWidget *parent = nullptr);
|
||||
void bind(UserListManager *manager);
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] UserListWidget *getUserList() const;
|
||||
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
|
||||
private:
|
||||
void persistExpandedSections(const QString §ionId, bool expanded);
|
||||
|
||||
QLineEdit *searchBar = nullptr;
|
||||
UserListWidget *userList = nullptr;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_LIST_PANEL_WIDGET_H
|
||||
|
|
@ -11,6 +11,9 @@
|
|||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
|
|
@ -24,6 +27,7 @@
|
|||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QScreen>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
|
@ -324,6 +328,10 @@ constexpr int Online = Qt::UserRole + 1;
|
|||
constexpr int UserInfo = Qt::UserRole + 2;
|
||||
} // namespace UserListRoles
|
||||
|
||||
// Divider items (section headers) in sectioned mode are distinguished from user
|
||||
// rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type.
|
||||
constexpr int SectionItemType = QTreeWidgetItem::UserType + 1;
|
||||
|
||||
UserListItemDelegate::UserListItemDelegate(QObject *const parent,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
|
|
@ -353,6 +361,9 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
|
|||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return QStyledItemDelegate::sizeHint(option, index);
|
||||
}
|
||||
if (!index.data(UserListRoles::UserInfo).isValid()) {
|
||||
return QStyledItemDelegate::sizeHint(option, index); // section dividers stay compact
|
||||
}
|
||||
return UserListPainter::sizeHint();
|
||||
}
|
||||
|
||||
|
|
@ -522,9 +533,18 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
|
||||
// Pin on item click
|
||||
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||
// Clicking a section divider toggles it
|
||||
if (sectioned && item->type() == SectionItemType) {
|
||||
setExpandedProgrammatically(item, !item->isExpanded());
|
||||
handleSectionExpansion(item, item->isExpanded());
|
||||
return;
|
||||
}
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
if (item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows have no user popup
|
||||
}
|
||||
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
|
||||
m_popupPinned = false; // reset so showPopupForUser can update
|
||||
showPopupForUser(name);
|
||||
|
|
@ -540,6 +560,14 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
}
|
||||
});
|
||||
|
||||
// Section dividers can be collapsed/expanded by the user. Surface those
|
||||
// changes (only from real user interaction — programmatic expansion is
|
||||
// applied through setSectionExpanded() / setExpandedProgrammatically()).
|
||||
connect(userTree, &QTreeWidget::itemExpanded, this,
|
||||
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); });
|
||||
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
||||
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); });
|
||||
|
||||
// Hide popup when list scrolls (reference row has moved)
|
||||
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
|
||||
m_showPopupTimer->stop();
|
||||
|
|
@ -572,14 +600,15 @@ void UserListWidget::bind(UserListManager *mgr)
|
|||
// ── Full rebuild: disconnect / reconnect / bulk initial load ──────────────
|
||||
connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild);
|
||||
|
||||
// ── Online users list (AllUsersList / RoomList) ───────────────────────────
|
||||
if (!sectioned) {
|
||||
// ── Online users list (AllUsersList / RoomList) ───────────────────────
|
||||
if (type == AllUsersList || type == RoomList) {
|
||||
connect(manager, &UserListManager::userJoinedOnline, this,
|
||||
[this](const ServerInfo_User &user) { processUserInfo(user, true); });
|
||||
connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); });
|
||||
}
|
||||
|
||||
// ── Buddy list ────────────────────────────────────────────────────────────
|
||||
// ── Buddy list ────────────────────────────────────────────────────────
|
||||
if (type == BuddyList) {
|
||||
connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) {
|
||||
const QString name = QString::fromStdString(user.name());
|
||||
|
|
@ -602,7 +631,7 @@ void UserListWidget::bind(UserListManager *mgr)
|
|||
});
|
||||
}
|
||||
|
||||
// ── Ignore list ───────────────────────────────────────────────────────────
|
||||
// ── Ignore list ───────────────────────────────────────────────────────
|
||||
if (type == IgnoreList) {
|
||||
connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) {
|
||||
const QString name = QString::fromStdString(user.name());
|
||||
|
|
@ -611,6 +640,24 @@ void UserListWidget::bind(UserListManager *mgr)
|
|||
connect(manager, &UserListManager::removedFromIgnoreList, this,
|
||||
[this](const QString &name) { deleteUser(name); });
|
||||
}
|
||||
} else {
|
||||
// ── Sectioned mode: one tree, every source feeds its own section. ─────
|
||||
// Presence drives the "online" section and the online state of buddies
|
||||
// and ignored users. Buddy/ignore mutations move users between sections
|
||||
// (priority: ignored > buddy > online).
|
||||
connect(manager, &UserListManager::userJoinedOnline, this,
|
||||
[this](const ServerInfo_User &user) { handleOnlineChange(user, true); });
|
||||
connect(manager, &UserListManager::userLeftOnline, this,
|
||||
[this](const QString &name) { handleOnlineChangeLeft(name); });
|
||||
connect(manager, &UserListManager::addedToBuddyList, this,
|
||||
[this](const ServerInfo_User &user) { handleListAdd(QStringLiteral("buddy"), user); });
|
||||
connect(manager, &UserListManager::removedFromBuddyList, this,
|
||||
[this](const QString &name) { handleListRemove(QStringLiteral("buddy"), name); });
|
||||
connect(manager, &UserListManager::addedToIgnoreList, this,
|
||||
[this](const ServerInfo_User &user) { handleListAdd(QStringLiteral("ignore"), user); });
|
||||
connect(manager, &UserListManager::removedFromIgnoreList, this,
|
||||
[this](const QString &name) { handleListRemove(QStringLiteral("ignore"), name); });
|
||||
}
|
||||
|
||||
// ── Popup button refresh ──────────────────────────────────────────────────
|
||||
// Any buddy/ignore mutation while the popup is open refreshes its buttons
|
||||
|
|
@ -736,8 +783,11 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
|||
return QGroupBox::eventFilter(obj, event);
|
||||
}
|
||||
auto *me = static_cast<QMouseEvent *>(event);
|
||||
auto *twi = static_cast<UserListTWI *>(userTree->itemAt(me->pos()));
|
||||
const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{};
|
||||
QTreeWidgetItem *hoveredItem = userTree->itemAt(me->pos());
|
||||
QString hovName;
|
||||
if (hoveredItem && hoveredItem->type() == QTreeWidgetItem::Type) {
|
||||
hovName = QString::fromStdString(static_cast<UserListTWI *>(hoveredItem)->getUserInfo().name());
|
||||
}
|
||||
|
||||
if (hovName != m_hoveredUser) {
|
||||
m_hoveredUser = hovName;
|
||||
|
|
@ -920,6 +970,23 @@ bool UserListWidget::isItemNearViewport(const UserListTWI *item) const
|
|||
|
||||
void UserListWidget::requestAvatarsForVisibleItems()
|
||||
{
|
||||
if (sectioned) {
|
||||
// Top-level items are dividers, user rows hang below them.
|
||||
for (const QString §ionId : sectionIds) {
|
||||
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
||||
if (!divider) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
auto *twi = static_cast<UserListTWI *>(divider->child(i));
|
||||
if (isItemNearViewport(twi)) {
|
||||
avatarProvider->requestAvatar(QString::fromStdString(twi->getUserInfo().name()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < userTree->topLevelItemCount(); ++i) {
|
||||
auto *twi = static_cast<UserListTWI *>(userTree->topLevelItem(i));
|
||||
if (isItemNearViewport(twi)) {
|
||||
|
|
@ -935,10 +1002,39 @@ void UserListWidget::rebuild()
|
|||
cardArtParamsMap.clear();
|
||||
onlineCount = 0;
|
||||
|
||||
if (sectioned) {
|
||||
createSectionItems();
|
||||
}
|
||||
|
||||
if (!manager) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectioned) {
|
||||
// Feed the sections in priority order (online first, then buddy, then
|
||||
// ignore): users present in several sources end up in the highest
|
||||
// priority section because processUserInfo() reparents existing items.
|
||||
beginBulkLoad();
|
||||
const auto &onlineUsers = manager->getAllUsersList();
|
||||
for (auto it = onlineUsers.cbegin(); it != onlineUsers.cend(); ++it) {
|
||||
processUserInfo(QStringLiteral("online"), it.value(), true);
|
||||
}
|
||||
const auto &buddyUsers = manager->getBuddyList();
|
||||
for (auto it = buddyUsers.cbegin(); it != buddyUsers.cend(); ++it) {
|
||||
if (manager->isUserIgnored(it.key())) {
|
||||
continue; // handled by the ignore pass below
|
||||
}
|
||||
processUserInfo(QStringLiteral("buddy"), it.value(), manager->getOnlineUser(it.key()) != nullptr);
|
||||
}
|
||||
const auto &ignoreUsers = manager->getIgnoreList();
|
||||
for (auto it = ignoreUsers.cbegin(); it != ignoreUsers.cend(); ++it) {
|
||||
processUserInfo(QStringLiteral("ignore"), it.value(), manager->getOnlineUser(it.key()) != nullptr);
|
||||
}
|
||||
endBulkLoad();
|
||||
applyFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
const QMap<QString, ServerInfo_User> *source = nullptr;
|
||||
|
||||
switch (type) {
|
||||
|
|
@ -959,12 +1055,11 @@ void UserListWidget::rebuild()
|
|||
processUserInfo(it.value(), manager->getOnlineUser(it.key()) != nullptr);
|
||||
}
|
||||
endBulkLoad();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
|
||||
void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QString &userName)
|
||||
{
|
||||
const QString userName = QString::fromStdString(user.name());
|
||||
|
||||
// Always update params from the latest ServerInfo_User, whether the
|
||||
// item is new or existing, so a live server-push refreshes the rendering.
|
||||
if (user.has_card_art_params()) {
|
||||
|
|
@ -981,6 +1076,13 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
|
|||
} else {
|
||||
cardArtParamsMap.remove(userName); // clear stale params on removal
|
||||
}
|
||||
}
|
||||
|
||||
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
|
||||
{
|
||||
const QString userName = QString::fromStdString(user.name());
|
||||
|
||||
updateCardArtParams(user, userName);
|
||||
|
||||
UserListTWI *item = users.value(userName);
|
||||
if (item) {
|
||||
|
|
@ -1000,6 +1102,44 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
|
|||
item->setOnline(online);
|
||||
if (!m_bulkLoading) {
|
||||
sortItems();
|
||||
applyFilter();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
void UserListWidget::processUserInfo(const QString §ionId, const ServerInfo_User &user, bool online)
|
||||
{
|
||||
const QString userName = QString::fromStdString(user.name());
|
||||
|
||||
updateCardArtParams(user, userName);
|
||||
|
||||
QTreeWidgetItem *sectionItem = sectionItems.value(sectionId);
|
||||
if (!sectionItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserListTWI *item = users.value(userName);
|
||||
if (item) {
|
||||
item->setUserInfo(user);
|
||||
if (item->parent() != sectionItem) {
|
||||
sectionItem->addChild(item); // reparent to the section it now belongs to
|
||||
}
|
||||
} else {
|
||||
item = new UserListTWI(user);
|
||||
users.insert(userName, item);
|
||||
sectionItem->addChild(item);
|
||||
if (online) {
|
||||
++onlineCount;
|
||||
}
|
||||
updateCount();
|
||||
if (!m_bulkLoading && isItemNearViewport(item)) {
|
||||
avatarProvider->requestAvatar(userName);
|
||||
}
|
||||
}
|
||||
item->setOnline(online);
|
||||
if (!m_bulkLoading) {
|
||||
sortItems();
|
||||
applyFilter();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
}
|
||||
|
|
@ -1012,12 +1152,17 @@ bool UserListWidget::deleteUser(const QString &userName)
|
|||
}
|
||||
|
||||
users.remove(userName);
|
||||
if (twi->parent()) {
|
||||
twi->parent()->removeChild(twi); // sectioned mode: rows hang off a divider
|
||||
} else {
|
||||
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
|
||||
if (twi->data(0, Qt::UserRole + 1).toBool()) {
|
||||
}
|
||||
if (twi->data(0, UserListRoles::Online).toBool()) {
|
||||
--onlineCount;
|
||||
}
|
||||
delete twi;
|
||||
updateCount();
|
||||
applyFilter();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1039,22 +1184,111 @@ void UserListWidget::setUserOnline(const QString &userName, bool online)
|
|||
|
||||
void UserListWidget::updateCount()
|
||||
{
|
||||
if (sectioned) {
|
||||
// The dividers carry the section titles
|
||||
setTitle(QString());
|
||||
for (const QString §ionId : sectionIds) {
|
||||
updateSectionDivider(sectionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (showTitle) {
|
||||
QString str = titleStr;
|
||||
if ((type == BuddyList) || (type == IgnoreList)) {
|
||||
str = str.arg(onlineCount);
|
||||
}
|
||||
setTitle(str.arg(userTree->topLevelItemCount()));
|
||||
} else {
|
||||
setTitle(QString());
|
||||
}
|
||||
}
|
||||
|
||||
void UserListWidget::setShowTitle(bool showTitle)
|
||||
{
|
||||
this->showTitle = showTitle;
|
||||
updateCount();
|
||||
}
|
||||
|
||||
void UserListWidget::setFilterText(const QString &text)
|
||||
{
|
||||
if (filterText == text) {
|
||||
return;
|
||||
}
|
||||
filterText = text;
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
void UserListWidget::applyFilter()
|
||||
{
|
||||
if (sectioned) {
|
||||
const bool searching = !filterText.isEmpty();
|
||||
const QString lower = filterText.toLower();
|
||||
for (const QString §ionId : sectionIds) {
|
||||
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
||||
if (!divider) {
|
||||
continue;
|
||||
}
|
||||
int visible = 0;
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
auto *child = static_cast<UserListTWI *>(divider->child(i));
|
||||
const bool match =
|
||||
!searching || QString::fromStdString(child->getUserInfo().name()).toLower().contains(lower);
|
||||
child->setHidden(!match);
|
||||
if (match) {
|
||||
++visible;
|
||||
}
|
||||
}
|
||||
if (searching) {
|
||||
// During a search the sections with matches stay open and empty
|
||||
// sections disappear entirely. The persisted expansion state is
|
||||
// untouched and restored when the search is cleared.
|
||||
divider->setHidden(visible == 0);
|
||||
setExpandedProgrammatically(divider, visible > 0);
|
||||
} else {
|
||||
divider->setHidden(false);
|
||||
setExpandedProgrammatically(divider, expandedSections.contains(sectionId));
|
||||
}
|
||||
updateSectionDivider(sectionId);
|
||||
}
|
||||
requestAvatarsForVisibleItems();
|
||||
userTree->viewport()->update();
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterText.isEmpty()) {
|
||||
for (auto it = users.cbegin(); it != users.cend(); ++it) {
|
||||
it.value()->setHidden(false);
|
||||
}
|
||||
} else {
|
||||
const QString lower = filterText.toLower();
|
||||
for (auto it = users.cbegin(); it != users.cend(); ++it) {
|
||||
const bool match = QString::fromStdString(it.value()->getUserInfo().name()).toLower().contains(lower);
|
||||
it.value()->setHidden(!match);
|
||||
}
|
||||
}
|
||||
|
||||
requestAvatarsForVisibleItems();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
|
||||
void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
||||
{
|
||||
if (item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows open no chat
|
||||
}
|
||||
emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true);
|
||||
}
|
||||
|
||||
void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index)
|
||||
{
|
||||
const ServerInfo_User &userInfo = static_cast<UserListTWI *>(userTree->topLevelItem(index.row()))->getUserInfo();
|
||||
bool online = index.sibling(index.row(), 0).data(Qt::UserRole + 1).toBool();
|
||||
QTreeWidgetItem *item = userTree->itemFromIndex(index);
|
||||
if (!item || item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows have no user menu
|
||||
}
|
||||
const auto *userItem = static_cast<UserListTWI *>(item);
|
||||
const ServerInfo_User &userInfo = userItem->getUserInfo();
|
||||
const bool online = userItem->data(0, UserListRoles::Online).toBool();
|
||||
|
||||
userContextMenu->showContextMenu(pos, QString::fromStdString(userInfo.name()),
|
||||
UserLevelFlags(userInfo.user_level()), online);
|
||||
|
|
@ -1062,5 +1296,247 @@ void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index
|
|||
|
||||
void UserListWidget::sortItems()
|
||||
{
|
||||
if (sectioned) {
|
||||
// Sorting must stay inside each section so the dividers keep their
|
||||
// places as top-level items.
|
||||
for (auto it = sectionItems.cbegin(); it != sectionItems.cend(); ++it) {
|
||||
it.value()->sortChildren(0, Qt::AscendingOrder);
|
||||
}
|
||||
return;
|
||||
}
|
||||
userTree->sortItems(0, Qt::AscendingOrder);
|
||||
}
|
||||
|
||||
// ── Sectioned mode ──────────────────────────────────────────────────────────
|
||||
|
||||
void UserListWidget::setSectioned(const QStringList &ids)
|
||||
{
|
||||
if (sectioned || ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sectioned = true;
|
||||
sectionIds = ids;
|
||||
expandedSections.clear();
|
||||
for (const QString §ionId : sectionIds) {
|
||||
expandedSections.insert(sectionId); // everything starts expanded
|
||||
}
|
||||
|
||||
// The single tree owns scrolling and the dividers carry the section titles,
|
||||
// so the group box chrome and tree decorations collapse into a flat list.
|
||||
setFlat(true);
|
||||
setShowTitle(false);
|
||||
userTree->setFrameStyle(QFrame::NoFrame);
|
||||
// No tree branches: the dividers draw their own arrow glyph, so the rows can
|
||||
// sit flush with the left border.
|
||||
userTree->setRootIsDecorated(false);
|
||||
userTree->setIndentation(0);
|
||||
userTree->setAlternatingRowColors(false);
|
||||
if (auto *listLayout = layout()) {
|
||||
listLayout->setContentsMargins(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
createSectionItems();
|
||||
updateCount();
|
||||
}
|
||||
|
||||
void UserListWidget::createSectionItems()
|
||||
{
|
||||
sectionItems.clear();
|
||||
QSignalBlocker blocker(userTree); // no expansion signals while building
|
||||
for (const QString §ionId : sectionIds) {
|
||||
QTreeWidgetItem *divider = createSectionItem(sectionId);
|
||||
sectionItems.insert(sectionId, divider);
|
||||
divider->setExpanded(expandedSections.contains(sectionId));
|
||||
}
|
||||
}
|
||||
|
||||
QTreeWidgetItem *UserListWidget::createSectionItem(const QString §ionId)
|
||||
{
|
||||
Q_UNUSED(sectionId);
|
||||
auto *divider = new QTreeWidgetItem(SectionItemType);
|
||||
divider->setFlags(Qt::ItemIsEnabled);
|
||||
|
||||
QFont font = userTree->font();
|
||||
font.setBold(true);
|
||||
divider->setFont(0, font);
|
||||
// A little taller than a plain text row so the header reads as a section
|
||||
// separator without matching the full user-row height.
|
||||
divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16));
|
||||
QColor muted = palette().color(QPalette::WindowText);
|
||||
muted.setAlpha(170);
|
||||
divider->setForeground(0, QBrush(muted));
|
||||
|
||||
userTree->addTopLevelItem(divider);
|
||||
// QTreeWidgetItem::setFirstColumnSpanned() is a no-op while the item is
|
||||
// detached from the tree (Qt returns early when treeModel() is null), so it
|
||||
// must be called after addTopLevelItem(). Without the span the divider text
|
||||
// is confined to column 0 and gets elided in unstyled mode.
|
||||
divider->setFirstColumnSpanned(true);
|
||||
return divider;
|
||||
}
|
||||
|
||||
QString UserListWidget::sectionTitle(const QString §ionId) const
|
||||
{
|
||||
if (sectionId == QLatin1String("buddy")) {
|
||||
return tr("Buddies");
|
||||
}
|
||||
if (sectionId == QLatin1String("online")) {
|
||||
return tr("Online");
|
||||
}
|
||||
if (sectionId == QLatin1String("ignore")) {
|
||||
return tr("Ignored");
|
||||
}
|
||||
return sectionId;
|
||||
}
|
||||
|
||||
void UserListWidget::updateSectionDivider(const QString §ionId)
|
||||
{
|
||||
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
||||
if (!divider) {
|
||||
return;
|
||||
}
|
||||
int visible = 0;
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
if (!divider->child(i)->isHidden()) {
|
||||
++visible;
|
||||
}
|
||||
}
|
||||
// The tree draws no branches (rows are flush), so the divider carries its
|
||||
// own collapse arrow glyph.
|
||||
const QString arrow = divider->isExpanded() ? QStringLiteral("\u25BE") : QStringLiteral("\u25B8");
|
||||
divider->setText(0, tr("%1 %2 (%3)").arg(arrow, sectionTitle(sectionId)).arg(visible));
|
||||
}
|
||||
|
||||
void UserListWidget::handleSectionExpansion(QTreeWidgetItem *item, bool expanded)
|
||||
{
|
||||
if (!sectioned || item->type() != SectionItemType) {
|
||||
return;
|
||||
}
|
||||
const QString sectionId = sectionItems.key(item);
|
||||
if (sectionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (expanded) {
|
||||
expandedSections.insert(sectionId);
|
||||
} else {
|
||||
expandedSections.remove(sectionId);
|
||||
}
|
||||
updateSectionDivider(sectionId); // the arrow glyph follows the state
|
||||
emit sectionExpanded(sectionId, expanded);
|
||||
}
|
||||
|
||||
void UserListWidget::setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded)
|
||||
{
|
||||
QSignalBlocker blocker(userTree);
|
||||
item->setExpanded(expanded);
|
||||
}
|
||||
|
||||
void UserListWidget::setSectionExpanded(const QString §ionId, bool expanded)
|
||||
{
|
||||
if (!sectioned) {
|
||||
return;
|
||||
}
|
||||
if (expanded) {
|
||||
expandedSections.insert(sectionId);
|
||||
} else {
|
||||
expandedSections.remove(sectionId);
|
||||
}
|
||||
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
||||
if (!divider) {
|
||||
return;
|
||||
}
|
||||
QSignalBlocker blocker(userTree);
|
||||
divider->setExpanded(expanded);
|
||||
updateSectionDivider(sectionId); // the arrow glyph follows the state
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
|
||||
void UserListWidget::handleOnlineChange(const ServerInfo_User &user, bool online)
|
||||
{
|
||||
const QString name = QString::fromStdString(user.name());
|
||||
UserListTWI *item = users.value(name);
|
||||
if (item) {
|
||||
item->setUserInfo(user);
|
||||
setUserOnline(name, online);
|
||||
return;
|
||||
}
|
||||
|
||||
// A brand-new user lands in the highest-priority section they belong to.
|
||||
QString sectionId = QStringLiteral("online");
|
||||
if (manager->isUserIgnored(name)) {
|
||||
sectionId = QStringLiteral("ignore");
|
||||
} else if (manager->isUserBuddy(name)) {
|
||||
sectionId = QStringLiteral("buddy");
|
||||
}
|
||||
processUserInfo(sectionId, user, online);
|
||||
}
|
||||
|
||||
void UserListWidget::handleOnlineChangeLeft(const QString &userName)
|
||||
{
|
||||
if (!users.contains(userName)) {
|
||||
return;
|
||||
}
|
||||
// Buddies and ignored users persist as offline. Pain room members leave
|
||||
// the list entirely.
|
||||
if (manager->isUserBuddy(userName) || manager->isUserIgnored(userName)) {
|
||||
setUserOnline(userName, false);
|
||||
} else {
|
||||
deleteUser(userName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserListWidget::handleListAdd(const QString §ionId, const ServerInfo_User &user)
|
||||
{
|
||||
const QString name = QString::fromStdString(user.name());
|
||||
// Ignored status outranks buddy status in the single list.
|
||||
if (sectionId == QLatin1String("buddy") && manager->isUserIgnored(name)) {
|
||||
processUserInfo(QStringLiteral("ignore"), user, manager->getOnlineUser(name) != nullptr);
|
||||
return;
|
||||
}
|
||||
processUserInfo(sectionId, user, manager->getOnlineUser(name) != nullptr);
|
||||
}
|
||||
|
||||
void UserListWidget::handleListRemove(const QString §ionId, const QString &userName)
|
||||
{
|
||||
// A user leaving one list may still belong elsewhere:
|
||||
// ignored > buddy > online.
|
||||
if (sectionId == QLatin1String("buddy")) {
|
||||
if (manager->isUserIgnored(userName)) {
|
||||
moveToSection(QStringLiteral("ignore"), userName);
|
||||
return;
|
||||
}
|
||||
if (manager->getOnlineUser(userName)) {
|
||||
moveToSection(QStringLiteral("online"), userName);
|
||||
return;
|
||||
}
|
||||
deleteUser(userName);
|
||||
return;
|
||||
}
|
||||
if (sectionId == QLatin1String("ignore")) {
|
||||
if (manager->isUserBuddy(userName)) {
|
||||
moveToSection(QStringLiteral("buddy"), userName);
|
||||
return;
|
||||
}
|
||||
if (manager->getOnlineUser(userName)) {
|
||||
moveToSection(QStringLiteral("online"), userName);
|
||||
return;
|
||||
}
|
||||
deleteUser(userName);
|
||||
}
|
||||
}
|
||||
|
||||
void UserListWidget::moveToSection(const QString §ionId, const QString &userName)
|
||||
{
|
||||
UserListTWI *item = users.value(userName);
|
||||
QTreeWidgetItem *target = sectionItems.value(sectionId);
|
||||
if (!item || !target || item->parent() == target) {
|
||||
return;
|
||||
}
|
||||
target->addChild(item); // reparents: the item leaves its old section
|
||||
if (!m_bulkLoading) {
|
||||
sortItems();
|
||||
applyFilter();
|
||||
userTree->viewport()->update();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidgetItem>
|
||||
|
|
@ -168,6 +170,25 @@ private:
|
|||
bool isItemNearViewport(const UserListTWI *item) const;
|
||||
void requestAvatarsForVisibleItems();
|
||||
|
||||
// ── Sectioned mode (single tree with inline dividers) ────────────────────
|
||||
bool sectioned = false;
|
||||
QStringList sectionIds;
|
||||
QMap<QString, QTreeWidgetItem *> sectionItems;
|
||||
QSet<QString> expandedSections;
|
||||
void createSectionItems();
|
||||
QTreeWidgetItem *createSectionItem(const QString §ionId);
|
||||
[[nodiscard]] QString sectionTitle(const QString §ionId) const;
|
||||
void updateSectionDivider(const QString §ionId);
|
||||
void handleSectionExpansion(QTreeWidgetItem *item, bool expanded);
|
||||
void setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded);
|
||||
void handleOnlineChange(const ServerInfo_User &user, bool online);
|
||||
void handleOnlineChangeLeft(const QString &userName);
|
||||
void handleListAdd(const QString §ionId, const ServerInfo_User &user);
|
||||
void handleListRemove(const QString §ionId, const QString &userName);
|
||||
void moveToSection(const QString §ionId, const QString &userName);
|
||||
void updateCardArtParams(const ServerInfo_User &user, const QString &userName);
|
||||
void processUserInfo(const QString §ionId, const ServerInfo_User &user, bool online);
|
||||
|
||||
QMap<QString, UserListTWI *> users;
|
||||
TabSupervisor *tabSupervisor;
|
||||
AbstractClient *client;
|
||||
|
|
@ -177,7 +198,10 @@ private:
|
|||
UserContextMenu *userContextMenu;
|
||||
int onlineCount;
|
||||
QString titleStr;
|
||||
QString filterText;
|
||||
bool showTitle = true;
|
||||
void updateCount();
|
||||
void applyFilter();
|
||||
void refreshPopupButtons(const QString &userName);
|
||||
private slots:
|
||||
void userClicked(QTreeWidgetItem *item, int column);
|
||||
|
|
@ -189,6 +213,7 @@ signals:
|
|||
void addIgnore(const QString &userName);
|
||||
void removeIgnore(const QString &userName);
|
||||
void joinGameRequested(int gameId, int roomId, bool asSpectator);
|
||||
void sectionExpanded(const QString §ionId, bool expanded);
|
||||
|
||||
public:
|
||||
UserListWidget(TabSupervisor *_tabSupervisor,
|
||||
|
|
@ -205,6 +230,14 @@ public:
|
|||
void processUserInfo(const ServerInfo_User &user, bool online);
|
||||
bool deleteUser(const QString &userName);
|
||||
void setUserOnline(const QString &userName, bool online);
|
||||
void setFilterText(const QString &text);
|
||||
void setShowTitle(bool showTitle);
|
||||
void setSectioned(const QStringList &ids);
|
||||
void setSectionExpanded(const QString §ionId, bool expanded);
|
||||
[[nodiscard]] const QStringList &getSectionIds() const
|
||||
{
|
||||
return sectionIds;
|
||||
}
|
||||
[[nodiscard]] const QMap<QString, UserListTWI *> &getUsers() const
|
||||
{
|
||||
return users;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "../interface/widgets/server/chat_view/chat_view.h"
|
||||
#include "../interface/widgets/server/game_selector.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/server/user/user_list_panel_widget.h"
|
||||
#include "../interface/widgets/server/user/user_list_widget.h"
|
||||
#include "../main.h"
|
||||
#include "../utility/completer_utils.h"
|
||||
|
|
@ -60,23 +61,10 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
|||
tempMap.insert(info.room_id(), gameTypes);
|
||||
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
|
||||
|
||||
auto *tabs = new QTabWidget(this);
|
||||
|
||||
friendsList = new UserListWidget(tabSupervisor, client, UserListWidget::BuddyList);
|
||||
friendsList->bind(tabSupervisor->getUserListManager());
|
||||
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
|
||||
userList->bind(tabSupervisor->getUserListManager());
|
||||
ignoreList = new UserListWidget(tabSupervisor, client, UserListWidget::IgnoreList);
|
||||
ignoreList->bind(tabSupervisor->getUserListManager());
|
||||
|
||||
connect(friendsList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
|
||||
SIGNAL(openMessageDialog(const QString &, bool)));
|
||||
|
||||
tabs->addTab(friendsList, tr("Friends"));
|
||||
tabs->addTab(userList, tr("Online"));
|
||||
tabs->addTab(ignoreList, tr("Ignored"));
|
||||
userListPanel = new UserListPanelWidget(tabSupervisor, client, this);
|
||||
userListPanel->bind(tabSupervisor->getUserListManager());
|
||||
userList = userListPanel->getUserList();
|
||||
connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog);
|
||||
|
||||
chatView = new ChatView(tabSupervisor, nullptr, true, this);
|
||||
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
|
||||
|
|
@ -126,7 +114,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
|||
|
||||
auto *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(splitter, 3);
|
||||
hbox->addWidget(tabs, 1);
|
||||
hbox->addWidget(userListPanel, 1);
|
||||
|
||||
aLeaveRoom = new QAction(this);
|
||||
connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest);
|
||||
|
|
@ -181,7 +169,7 @@ void TabRoom::retranslateUi()
|
|||
{
|
||||
gameSelector->retranslateUi();
|
||||
chatView->retranslateUi();
|
||||
userList->retranslateUi();
|
||||
userListPanel->retranslateUi();
|
||||
sayLabel->setText(tr("&Say:"));
|
||||
chatGroupBox->setTitle(tr("Chat"));
|
||||
roomMenu->setTitle(tr("&Room"));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class Message;
|
|||
} // namespace google
|
||||
class AbstractClient;
|
||||
class UserListWidget;
|
||||
class UserListPanelWidget;
|
||||
class QLabel;
|
||||
class ChatView;
|
||||
class QPushButton;
|
||||
|
|
@ -57,9 +58,8 @@ private:
|
|||
QMap<int, QString> gameTypes;
|
||||
|
||||
GameSelector *gameSelector;
|
||||
UserListWidget *friendsList;
|
||||
UserListPanelWidget *userListPanel;
|
||||
UserListWidget *userList;
|
||||
UserListWidget *ignoreList;
|
||||
const UserListProxy *userListProxy;
|
||||
ChatView *chatView;
|
||||
QLabel *sayLabel;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class IInterfaceSettingsProvider
|
||||
{
|
||||
|
|
@ -41,6 +42,7 @@ public:
|
|||
[[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0;
|
||||
[[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0;
|
||||
[[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0;
|
||||
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -170,6 +170,12 @@ bool InterfaceSettings::getBattlefieldFlashEnabled() const
|
|||
return getValue("battlefieldFlashEnabled", QString(), QString(), true).toBool();
|
||||
}
|
||||
|
||||
QStringList InterfaceSettings::getUserListExpandedSections() const
|
||||
{
|
||||
return getValue("userListExpandedSections", QString(), QString(), QStringList({"buddy", "online", "ignore"}))
|
||||
.toStringList();
|
||||
}
|
||||
|
||||
void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus)
|
||||
{
|
||||
setValue(_useTearOffMenus, "useTearOffMenus");
|
||||
|
|
@ -348,3 +354,8 @@ void InterfaceSettings::setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled
|
|||
setValue(_battlefieldFlashEnabled, "battlefieldFlashEnabled");
|
||||
emit battlefieldFlashEnabledChanged(_battlefieldFlashEnabled);
|
||||
}
|
||||
|
||||
void InterfaceSettings::setUserListExpandedSections(const QStringList §ions)
|
||||
{
|
||||
setValue(sections, "userListExpandedSections");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public:
|
|||
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
|
||||
[[nodiscard]] bool getLifeCounterAnimationsEnabled() const override;
|
||||
[[nodiscard]] bool getBattlefieldFlashEnabled() const override;
|
||||
[[nodiscard]] QStringList getUserListExpandedSections() const override;
|
||||
|
||||
void setUseTearOffMenus(bool _useTearOffMenus);
|
||||
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
|
||||
|
|
@ -78,6 +79,7 @@ public:
|
|||
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
|
||||
void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled);
|
||||
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
|
||||
void setUserListExpandedSections(const QStringList §ions);
|
||||
|
||||
signals:
|
||||
void useTearOffMenusChanged(bool state);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue