mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-29 17:32:17 -07:00
[UserList] Tune the role row gradient colors (dark parity, light mode)
Dark mode is byte-for-byte the pre-branch painter profile, with the original saturated-left to navy-right fade restored verbatim. Light mode uses the same language at high tint strength: role rows get colored fades (0.75/0.65 left to 0.18/0.10 right), and regular users get flat warm paper cards (AlternateBase) instead of the grey slate.
This commit is contained in:
parent
ac09f4bbaf
commit
75142e6e9f
6 changed files with 91 additions and 76 deletions
|
|
@ -322,7 +322,7 @@ UserInfoPopup::UserInfoPopup(TabSupervisor *_ts,
|
||||||
setFrameShape(QFrame::NoFrame);
|
setFrameShape(QFrame::NoFrame);
|
||||||
buildUi();
|
buildUi();
|
||||||
|
|
||||||
// Re-theme the popup chrome when the theme or its color scheme changes.
|
// Restyle the popup chrome when the theme or its color scheme changes.
|
||||||
if (themeManager) {
|
if (themeManager) {
|
||||||
connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme);
|
connect(themeManager, &ThemeManager::themeChanged, this, &UserInfoPopup::applyTheme);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ namespace PopupRoles
|
||||||
constexpr int GameData = Qt::UserRole + 10;
|
constexpr int GameData = Qt::UserRole + 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Popup theme ───────────────────────────────────────────────────────────────
|
// Popup theme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Palette-derived colors for the popup chrome. Both color schemes read from
|
* Palette-derived colors for the popup chrome. Both color schemes read from
|
||||||
* the active QPalette so custom palettes are respected; @c dark only tunes the
|
* the active QPalette so custom palettes are respected. @c dark only tunes the
|
||||||
* blend strengths.
|
* blend strengths.
|
||||||
*/
|
*/
|
||||||
struct PopupTheme
|
struct PopupTheme
|
||||||
|
|
|
||||||
|
|
@ -84,29 +84,41 @@ void UserListPainter::drawBackground(QPainter *painter,
|
||||||
const QRectF &cardRect,
|
const QRectF &cardRect,
|
||||||
const QColor &accentColor,
|
const QColor &accentColor,
|
||||||
bool selected,
|
bool selected,
|
||||||
const Style &style)
|
const Style &style,
|
||||||
|
bool hasRole)
|
||||||
{
|
{
|
||||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||||
if (selected) {
|
if (style.dark) {
|
||||||
bg.setColorAt(0, blend(style.cardStart, accentColor, 0.20));
|
// Dark mode darkens the role color to fit the dark surface and fades
|
||||||
bg.setColorAt(1, blend(style.cardEnd, accentColor, 0.13));
|
// it into the deep navy surface on the right. The text drop shadow
|
||||||
|
// keeps the username legible over the colored edge.
|
||||||
|
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
|
||||||
|
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
|
||||||
|
} else if (hasRole) {
|
||||||
|
// Light mode pegs the role color on the left at near full strength
|
||||||
|
// and fades it into the white surface on the right. The tint stays
|
||||||
|
// bright enough that the dark text remains legible without a shadow.
|
||||||
|
bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65));
|
||||||
|
bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10));
|
||||||
} else {
|
} else {
|
||||||
// Gentle accent tint so a role's color stays visible on unselected
|
// Regular users are the light theme's neutral paper cards. A flat
|
||||||
// cards without shouting over the list. Light cards need a slightly
|
// warm card fill (the normal row surface, slightly deepened) keeps
|
||||||
// stronger blend because the base is near-white.
|
// every row clearly visible without borrowing a role color. Selection
|
||||||
const qreal startT = style.dark ? 0.12 : 0.16;
|
// shifts the fill toward a soft slate so the highlight still reads.
|
||||||
const qreal endT = style.dark ? 0.08 : 0.10;
|
const QColor paper = style.cardEnd.darker(108);
|
||||||
bg.setColorAt(0, blend(style.cardStart, accentColor, startT));
|
bg.setColorAt(0, blend(paper, accentColor, selected ? 0.35 : 0.0));
|
||||||
bg.setColorAt(1, blend(style.cardEnd, accentColor, endT));
|
bg.setColorAt(1, blend(paper, accentColor, selected ? 0.25 : 0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
painter->setPen(Qt::NoPen);
|
painter->setPen(Qt::NoPen);
|
||||||
painter->setBrush(bg);
|
painter->setBrush(bg);
|
||||||
painter->drawRoundedRect(cardRect, 6, 6);
|
painter->drawRoundedRect(cardRect, 6, 6);
|
||||||
|
|
||||||
|
if (style.dark || hasRole || selected) {
|
||||||
painter->setBrush(accentColor);
|
painter->setBrush(accentColor);
|
||||||
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
|
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
|
||||||
{
|
{
|
||||||
|
|
@ -353,7 +365,7 @@ void UserListPainter::paint(QPainter *painter,
|
||||||
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||||
|
|
||||||
// The delegate supplies the application palette in option.palette, which
|
// The delegate supplies the application palette in option.palette, which
|
||||||
// always reflects the active theme — the widget palette can be stale after
|
// always reflects the active theme. The widget palette can be stale after
|
||||||
// a runtime theme change, so it is only used as a defensive fallback.
|
// a runtime theme change, so it is only used as a defensive fallback.
|
||||||
const QPalette pal =
|
const QPalette pal =
|
||||||
option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette;
|
option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette;
|
||||||
|
|
@ -366,6 +378,9 @@ void UserListPainter::paint(QPainter *painter,
|
||||||
const QString userName = QString::fromStdString(userInfo.name());
|
const QString userName = QString::fromStdString(userInfo.name());
|
||||||
const QString privLevel = QString::fromStdString(userInfo.privlevel());
|
const QString privLevel = QString::fromStdString(userInfo.privlevel());
|
||||||
const QColor accentColor = getAccentColor(userLevel, online);
|
const QColor accentColor = getAccentColor(userLevel, online);
|
||||||
|
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) ||
|
||||||
|
userLevel.testFlag(ServerInfo_User::IsModerator) ||
|
||||||
|
userLevel.testFlag(ServerInfo_User::IsJudge);
|
||||||
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
||||||
const int cardRight = getCardRight(option, rect);
|
const int cardRight = getCardRight(option, rect);
|
||||||
|
|
||||||
|
|
@ -373,7 +388,7 @@ void UserListPainter::paint(QPainter *painter,
|
||||||
? cardArtParamsMap->value(userName)
|
? cardArtParamsMap->value(userName)
|
||||||
: CardArtParams{};
|
: CardArtParams{};
|
||||||
|
|
||||||
drawBackground(painter, cardRect, accentColor, selected, style);
|
drawBackground(painter, cardRect, accentColor, selected, style, hasRole);
|
||||||
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
|
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
|
||||||
|
|
||||||
const QRect avatarRect = getAvatarRect(rect);
|
const QRect avatarRect = getAvatarRect(rect);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public:
|
||||||
/**
|
/**
|
||||||
* Palette-derived surface colors for the current color scheme. Both the
|
* Palette-derived surface colors for the current color scheme. Both the
|
||||||
* light and the dark scheme read from the active QPalette so custom
|
* light and the dark scheme read from the active QPalette so custom
|
||||||
* palettes are respected; @c dark only tunes the blend strengths (and
|
* palettes are respected. @c dark only tunes the blend strengths (and
|
||||||
* whether the name text keeps its drop shadow).
|
* whether the name text keeps its drop shadow).
|
||||||
*/
|
*/
|
||||||
struct Style
|
struct Style
|
||||||
|
|
@ -83,7 +83,8 @@ private:
|
||||||
const QRectF &cardRect,
|
const QRectF &cardRect,
|
||||||
const QColor &accentColor,
|
const QColor &accentColor,
|
||||||
bool selected,
|
bool selected,
|
||||||
const Style &style);
|
const Style &style,
|
||||||
|
bool hasRole);
|
||||||
static QRect getAvatarRect(const QRect &rect);
|
static QRect getAvatarRect(const QRect &rect);
|
||||||
static void drawAvatar(QPainter *painter,
|
static void drawAvatar(QPainter *painter,
|
||||||
const QRect &avatarRect,
|
const QRect &avatarRect,
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,7 @@ QSize UserListItemDelegate::sizeHint(const QStyleOptionViewItem &option, const Q
|
||||||
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||||
{
|
{
|
||||||
const bool styled = SettingsCache::instance().appearance().getStyleUserList();
|
const bool styled = SettingsCache::instance().appearance().getStyleUserList();
|
||||||
// UserInfo/Online are stored on column 0 only; the name lives on column 2,
|
// UserInfo/Online are stored on column 0 only. The name lives on column 2,
|
||||||
// so resolve the user data against column 0 no matter which cell is painted.
|
// so resolve the user data against column 0 no matter which cell is painted.
|
||||||
const QModelIndex userIndex = index.siblingAtColumn(0).isValid() ? index.siblingAtColumn(0) : index;
|
const QModelIndex userIndex = index.siblingAtColumn(0).isValid() ? index.siblingAtColumn(0) : index;
|
||||||
const QVariant var = userIndex.data(UserListRoles::UserInfo);
|
const QVariant var = userIndex.data(UserListRoles::UserInfo);
|
||||||
|
|
@ -391,9 +391,9 @@ void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unstyled rows and section dividers are painted manually so every color
|
// Unstyled rows and section dividers are painted manually so every color
|
||||||
// is derived from the current application palette at paint time: the widget
|
// is derived from the current application palette at paint time. The widget
|
||||||
// palette, the view's alternation, and the stored per-item brushes all go
|
// palette, the view's alternation, and the stored brushes (one per item)
|
||||||
// stale after a runtime theme change.
|
// all go stale after a runtime theme change.
|
||||||
const QPalette appPal = qApp->palette();
|
const QPalette appPal = qApp->palette();
|
||||||
const bool selected = option.state & QStyle::State_Selected;
|
const bool selected = option.state & QStyle::State_Selected;
|
||||||
const bool hovered = option.state & QStyle::State_MouseOver;
|
const bool hovered = option.state & QStyle::State_MouseOver;
|
||||||
|
|
@ -405,11 +405,11 @@ void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||||
} else if (hovered) {
|
} else if (hovered) {
|
||||||
bg = UserListPainter::blend(appPal.color(QPalette::Base), appPal.color(QPalette::Highlight), 0.12);
|
bg = UserListPainter::blend(appPal.color(QPalette::Base), appPal.color(QPalette::Highlight), 0.12);
|
||||||
} else if (var.isValid()) {
|
} else if (var.isValid()) {
|
||||||
// Zebra with per-section parity: the dividers are top-level rows too,
|
// Zebra alternation per section. The dividers are top level rows too,
|
||||||
// so the view's own alternation would drift across sections. Count the
|
// so the view's own alternation would drift across sections. Count the
|
||||||
// visible user rows back to the previous divider within the same parent
|
// visible user rows back to the previous divider within the same parent
|
||||||
// (sectioned mode stores users as children of the dividers, flat mode
|
// (sectioned mode stores users as children of the dividers, flat mode
|
||||||
// as top-level rows), so the stripe restarts at every divider and at
|
// as top level rows), so the stripe restarts at every divider and at
|
||||||
// the tree top. Hidden filter matches are skipped the same way the view
|
// the tree top. Hidden filter matches are skipped the same way the view
|
||||||
// skips them, so adjacent visible rows always alternate.
|
// skips them, so adjacent visible rows always alternate.
|
||||||
int usersSinceDivider = 0;
|
int usersSinceDivider = 0;
|
||||||
|
|
@ -429,9 +429,9 @@ void UserListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||||
bg = appPal.color(QPalette::AlternateBase);
|
bg = appPal.color(QPalette::AlternateBase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Paint the row background. In the column-0 pass the fill spans the full
|
// Paint the row background. In the column 0 pass the fill spans the full
|
||||||
// viewport width so stripes, hover and selection cover the whole row (the
|
// viewport width so stripes, hover and selection cover the whole row (the
|
||||||
// name column is content-sized in unstyled mode); later column passes fill
|
// name column is content sized in unstyled mode). Later column passes fill
|
||||||
// only their own cell, which is the same color and cannot cover the icons.
|
// only their own cell, which is the same color and cannot cover the icons.
|
||||||
QRect bgRect = option.rect;
|
QRect bgRect = option.rect;
|
||||||
if (index.column() == 0) {
|
if (index.column() == 0) {
|
||||||
|
|
@ -619,9 +619,9 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
if (hoveredUser.isEmpty()) {
|
if (hoveredUser.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Re-resolve the row under the cursor: in sectioned mode a user can own
|
// Resolve the row under the cursor again. In sectioned mode a user can
|
||||||
// several rows (online + buddy), so the popup must anchor to the exact
|
// own several rows (online + buddy), so the popup must anchor to the
|
||||||
// hovered row instead of a name-based lookup.
|
// exact hovered row instead of a lookup by name.
|
||||||
const QPoint viewportPos = userTree->viewport()->mapFromGlobal(QCursor::pos());
|
const QPoint viewportPos = userTree->viewport()->mapFromGlobal(QCursor::pos());
|
||||||
QTreeWidgetItem *item = userTree->itemAt(viewportPos);
|
QTreeWidgetItem *item = userTree->itemAt(viewportPos);
|
||||||
if (item && item->type() == QTreeWidgetItem::Type &&
|
if (item && item->type() == QTreeWidgetItem::Type &&
|
||||||
|
|
@ -667,7 +667,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
|
|
||||||
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||||
[this](const QItemSelection &sel, const QItemSelection &) {
|
[this](const QItemSelection &sel, const QItemSelection &) {
|
||||||
// if (m_rebuildingTree) return;
|
|
||||||
if (sel.isEmpty() && popupPinned) {
|
if (sel.isEmpty() && popupPinned) {
|
||||||
popupPinned = false;
|
popupPinned = false;
|
||||||
hidePopup();
|
hidePopup();
|
||||||
|
|
@ -676,7 +675,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
|
|
||||||
// Keyboard selection: show the popup for the current row and hide it when
|
// Keyboard selection: show the popup for the current row and hide it when
|
||||||
// the focus moves to a section divider or leaves the list entirely. The
|
// the focus moves to a section divider or leaves the list entirely. The
|
||||||
// popup therefore follows arrow-key navigation exactly like mouse hover.
|
// popup therefore follows arrow key navigation exactly like mouse hover.
|
||||||
// When it was pinned by a click it stays open and follows the selection.
|
// When it was pinned by a click it stays open and follows the selection.
|
||||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||||
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
||||||
|
|
@ -691,8 +690,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Section dividers can be collapsed/expanded by the user. Surface those
|
// Section dividers can be collapsed/expanded by the user. Surface those
|
||||||
// changes (only from real user interaction — programmatic expansion is
|
// changes only from real user interaction. Programmatic expansion is
|
||||||
// applied through setSectionExpanded() / setExpandedProgrammatically()).
|
// applied through setSectionExpanded() / setExpandedProgrammatically().
|
||||||
connect(userTree, &QTreeWidget::itemExpanded, this,
|
connect(userTree, &QTreeWidget::itemExpanded, this,
|
||||||
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); });
|
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, true); });
|
||||||
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
||||||
|
|
@ -742,14 +741,14 @@ void UserListWidget::bind(UserListManager *mgr)
|
||||||
connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild);
|
connect(manager, &UserListManager::listReset, this, &UserListWidget::rebuild);
|
||||||
|
|
||||||
if (!sectioned) {
|
if (!sectioned) {
|
||||||
// ── Online users list (AllUsersList / RoomList) ───────────────────────
|
// Online users list (AllUsersList / RoomList)
|
||||||
if (type == AllUsersList || type == RoomList) {
|
if (type == AllUsersList || type == RoomList) {
|
||||||
connect(manager, &UserListManager::userJoinedOnline, this,
|
connect(manager, &UserListManager::userJoinedOnline, this,
|
||||||
[this](const ServerInfo_User &user) { processUserInfo(user, true); });
|
[this](const ServerInfo_User &user) { processUserInfo(user, true); });
|
||||||
connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); });
|
connect(manager, &UserListManager::userLeftOnline, this, [this](const QString &name) { deleteUser(name); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Buddy list ────────────────────────────────────────────────────────
|
// Buddy list
|
||||||
if (type == BuddyList) {
|
if (type == BuddyList) {
|
||||||
connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) {
|
connect(manager, &UserListManager::addedToBuddyList, this, [this](const ServerInfo_User &user) {
|
||||||
const QString name = QString::fromStdString(user.name());
|
const QString name = QString::fromStdString(user.name());
|
||||||
|
|
@ -772,7 +771,7 @@ void UserListWidget::bind(UserListManager *mgr)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ignore list ───────────────────────────────────────────────────────
|
// Ignore list
|
||||||
if (type == IgnoreList) {
|
if (type == IgnoreList) {
|
||||||
connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) {
|
connect(manager, &UserListManager::addedToIgnoreList, this, [this](const ServerInfo_User &user) {
|
||||||
const QString name = QString::fromStdString(user.name());
|
const QString name = QString::fromStdString(user.name());
|
||||||
|
|
@ -782,7 +781,7 @@ void UserListWidget::bind(UserListManager *mgr)
|
||||||
[this](const QString &name) { deleteUser(name); });
|
[this](const QString &name) { deleteUser(name); });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ── Sectioned mode: one tree, every source feeds its own section. ─────
|
// Sectioned mode: one tree, every source feeds its own section.
|
||||||
// Sections are pure membership views: the "Online" section holds every
|
// Sections are pure membership views: the "Online" section holds every
|
||||||
// currently online user, the "Buddy"/"Ignore" sections hold those
|
// currently online user, the "Buddy"/"Ignore" sections hold those
|
||||||
// lists. A user can therefore appear in several sections at once (an
|
// lists. A user can therefore appear in several sections at once (an
|
||||||
|
|
@ -843,7 +842,7 @@ void UserListWidget::refreshPopupButtons(const QString &userName)
|
||||||
const bool isIgn = proxy->isUserIgnored(userName);
|
const bool isIgn = proxy->isUserIgnored(userName);
|
||||||
|
|
||||||
userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn);
|
userInfoPopup->updateActionButtons(item->getUserInfo(), online, isBuddy, isIgn);
|
||||||
positionPopup(item); // height may have changed — reposition
|
positionPopup(item); // height may have changed, reposition
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserListWidget::hideEvent(QHideEvent *e)
|
void UserListWidget::hideEvent(QHideEvent *e)
|
||||||
|
|
@ -919,8 +918,8 @@ void UserListWidget::connectPopupSignals()
|
||||||
|
|
||||||
bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
||||||
{
|
{
|
||||||
// ── Keyboard navigation of the section dividers ──────────────────────────
|
// Keyboard navigation of the section dividers.
|
||||||
// The dividers are selectable so arrow keys land on them; when one is the
|
// The dividers are selectable so arrow keys land on them. When one is the
|
||||||
// current item, Enter/Space toggle it (like a button) and Left/Right follow
|
// current item, Enter/Space toggle it (like a button) and Left/Right follow
|
||||||
// the tree convention (Left collapses, Right expands).
|
// the tree convention (Left collapses, Right expands).
|
||||||
if (obj == userTree && event->type() == QEvent::KeyPress) {
|
if (obj == userTree && event->type() == QEvent::KeyPress) {
|
||||||
|
|
@ -992,16 +991,16 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
|
||||||
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName);
|
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName);
|
||||||
const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName);
|
const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName);
|
||||||
|
|
||||||
// The popup is already showing this user (e.g. arrow-key navigation between
|
// The popup is already showing this user (e.g. arrow key navigation between
|
||||||
// the online/buddy rows of the same user): just re-anchor it.
|
// the online/buddy rows of the same user): just reposition it.
|
||||||
if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == userName) {
|
if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == userName) {
|
||||||
positionPopup(item);
|
positionPopup(item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel any pending show/hide so a hover timer that armed before a
|
// Cancel any pending show/hide so a hover timer that armed before a row
|
||||||
// keyboard-selected row cannot override it, and a pending hide cannot kill
|
// selected via the keyboard cannot override it, and a pending hide cannot
|
||||||
// the popup right after it appears.
|
// kill the popup right after it appears.
|
||||||
showPopupTimer->stop();
|
showPopupTimer->stop();
|
||||||
hidePopupTimer->stop();
|
hidePopupTimer->stop();
|
||||||
|
|
||||||
|
|
@ -1013,17 +1012,18 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
|
||||||
// 1) move() applies to an existing native handle (not overridden by
|
// 1) move() applies to an existing native handle (not overridden by
|
||||||
// Qt's default centering logic on first show)
|
// Qt's default centering logic on first show)
|
||||||
// 2) adjustSize() inside positionPopup() can measure the final
|
// 2) adjustSize() inside positionPopup() can measure the final
|
||||||
// laid-out geometry correctly
|
// laid out geometry correctly
|
||||||
userInfoPopup->setWindowOpacity(0.0);
|
userInfoPopup->setWindowOpacity(0.0);
|
||||||
}
|
}
|
||||||
userInfoPopup->show();
|
userInfoPopup->show();
|
||||||
userInfoPopup->raise();
|
userInfoPopup->raise();
|
||||||
|
|
||||||
positionPopup(item); // geometry is now accurate; move() sticks
|
positionPopup(item); // geometry is accurate after show, so move() is not overridden
|
||||||
|
|
||||||
if (wasVisible) {
|
if (wasVisible) {
|
||||||
// Content swap while already open (hover/arrow-key navigation): keep
|
// Content swap while already open (hover or arrow key navigation):
|
||||||
// the popup opaque instead of flashing through a fade on every step.
|
// keep the popup opaque instead of flashing through a fade on every
|
||||||
|
// step.
|
||||||
userInfoPopup->setWindowOpacity(1.0);
|
userInfoPopup->setWindowOpacity(1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1157,7 +1157,7 @@ bool UserListWidget::isItemNearViewport(const UserListTWI *item) const
|
||||||
void UserListWidget::requestAvatarsForVisibleItems()
|
void UserListWidget::requestAvatarsForVisibleItems()
|
||||||
{
|
{
|
||||||
if (sectioned) {
|
if (sectioned) {
|
||||||
// Top-level items are dividers, user rows hang below them.
|
// Top level items are dividers, user rows hang below them.
|
||||||
for (const QString §ionId : sectionIds) {
|
for (const QString §ionId : sectionIds) {
|
||||||
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
QTreeWidgetItem *divider = sectionItems.value(sectionId);
|
||||||
if (!divider) {
|
if (!divider) {
|
||||||
|
|
@ -1198,7 +1198,7 @@ void UserListWidget::rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sectioned) {
|
if (sectioned) {
|
||||||
// Every source feeds its own section; users that belong to several
|
// Every source feeds its own section. Users that belong to several
|
||||||
// sources (an online buddy) get one row per section because
|
// sources (an online buddy) get one row per section because
|
||||||
// ensureSectionMembership() creates the row when it is missing.
|
// ensureSectionMembership() creates the row when it is missing.
|
||||||
beginBulkLoad();
|
beginBulkLoad();
|
||||||
|
|
@ -1245,7 +1245,7 @@ void UserListWidget::rebuild()
|
||||||
void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QString &userName)
|
void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QString &userName)
|
||||||
{
|
{
|
||||||
// Always update params from the latest ServerInfo_User, whether the
|
// Always update params from the latest ServerInfo_User, whether the
|
||||||
// item is new or existing, so a live server-push refreshes the rendering.
|
// item is new or existing, so a live server push refreshes the rendering.
|
||||||
if (user.has_card_art_params()) {
|
if (user.has_card_art_params()) {
|
||||||
const auto &cap = user.card_art_params();
|
const auto &cap = user.card_art_params();
|
||||||
CardArtParams params;
|
CardArtParams params;
|
||||||
|
|
@ -1304,7 +1304,7 @@ void UserListWidget::processUserInfo(const QString §ionId, const ServerInfo_
|
||||||
bool UserListWidget::deleteUser(const QString &userName)
|
bool UserListWidget::deleteUser(const QString &userName)
|
||||||
{
|
{
|
||||||
if (sectioned) {
|
if (sectioned) {
|
||||||
// The user may own several rows (one per section); drop them all.
|
// The user may own several rows (one per section). Drop them all.
|
||||||
bool removed = false;
|
bool removed = false;
|
||||||
const QStringList sections = sectionUsers.keys(); // snapshot: maps mutate
|
const QStringList sections = sectionUsers.keys(); // snapshot: maps mutate
|
||||||
for (const QString §ionId : sections) {
|
for (const QString §ionId : sections) {
|
||||||
|
|
@ -1342,7 +1342,7 @@ void UserListWidget::setUserOnline(const QString &userName, bool online)
|
||||||
{
|
{
|
||||||
if (sectioned) {
|
if (sectioned) {
|
||||||
// The rows in the "Online" section are created/removed by the presence
|
// The rows in the "Online" section are created/removed by the presence
|
||||||
// handlers; this only keeps the presence flag of the surviving rows
|
// handlers. This only keeps the presence flag of the surviving rows
|
||||||
// (e.g. a buddy row after the user went offline) in sync.
|
// (e.g. a buddy row after the user went offline) in sync.
|
||||||
for (auto it = sectionUsers.cbegin(); it != sectionUsers.cend(); ++it) {
|
for (auto it = sectionUsers.cbegin(); it != sectionUsers.cend(); ++it) {
|
||||||
UserListTWI *item = it.value().value(userName);
|
UserListTWI *item = it.value().value(userName);
|
||||||
|
|
@ -1358,9 +1358,9 @@ void UserListWidget::setUserOnline(const QString &userName, bool online)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No state change: nothing to re-sort. This also keeps the per-user
|
// No state change: nothing to resort. This also keeps the presence
|
||||||
// presence broadcasts (userJoinedOnline fires for every online user) cheap
|
// broadcasts cheap (userJoinedOnline fires once per online user) when the
|
||||||
// when the row already carries the right flag.
|
// row already carries the right flag.
|
||||||
if (twi->data(0, UserListRoles::Online).toBool() == online) {
|
if (twi->data(0, UserListRoles::Online).toBool() == online) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1374,8 +1374,7 @@ void UserListWidget::setUserOnline(const QString &userName, bool online)
|
||||||
updateCount();
|
updateCount();
|
||||||
|
|
||||||
// Online users sort above offline users (UserListTWI::operator<), so a
|
// Online users sort above offline users (UserListTWI::operator<), so a
|
||||||
// flag change moves the row: re-sort so e.g. a buddy who went offline no
|
// flag change moves the row. Resort to place the user by the new state.
|
||||||
// longer stays pinned at the top of the list.
|
|
||||||
if (!bulkLoading) {
|
if (!bulkLoading) {
|
||||||
sortItems();
|
sortItems();
|
||||||
applyFilter();
|
applyFilter();
|
||||||
|
|
@ -1499,7 +1498,7 @@ void UserListWidget::sortItems()
|
||||||
{
|
{
|
||||||
if (sectioned) {
|
if (sectioned) {
|
||||||
// Sorting must stay inside each section so the dividers keep their
|
// Sorting must stay inside each section so the dividers keep their
|
||||||
// places as top-level items.
|
// places as top level items.
|
||||||
for (auto it = sectionItems.cbegin(); it != sectionItems.cend(); ++it) {
|
for (auto it = sectionItems.cbegin(); it != sectionItems.cend(); ++it) {
|
||||||
it.value()->sortChildren(0, Qt::AscendingOrder);
|
it.value()->sortChildren(0, Qt::AscendingOrder);
|
||||||
}
|
}
|
||||||
|
|
@ -1508,7 +1507,7 @@ void UserListWidget::sortItems()
|
||||||
userTree->sortItems(0, Qt::AscendingOrder);
|
userTree->sortItems(0, Qt::AscendingOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sectioned mode ──────────────────────────────────────────────────────────
|
// Sectioned mode
|
||||||
|
|
||||||
void UserListWidget::setSectioned(const QStringList &ids)
|
void UserListWidget::setSectioned(const QStringList &ids)
|
||||||
{
|
{
|
||||||
|
|
@ -1556,19 +1555,19 @@ QTreeWidgetItem *UserListWidget::createSectionItem(const QString §ionId)
|
||||||
{
|
{
|
||||||
Q_UNUSED(sectionId);
|
Q_UNUSED(sectionId);
|
||||||
auto *divider = new QTreeWidgetItem(SectionItemType);
|
auto *divider = new QTreeWidgetItem(SectionItemType);
|
||||||
// Selectable so keyboard navigation (Up/Down) can land on the dividers;
|
// Selectable so keyboard navigation (Up/Down) can land on the dividers.
|
||||||
// they act as collapsible section headers once they have focus.
|
// They act as collapsible section headers once they have focus.
|
||||||
divider->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
divider->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
||||||
|
|
||||||
QFont font = userTree->font();
|
QFont font = userTree->font();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
divider->setFont(0, font);
|
divider->setFont(0, font);
|
||||||
// A little taller than a plain text row so the header reads as a section
|
// A little taller than a plain text row so the header reads as a section
|
||||||
// separator without matching the full user-row height.
|
// separator without matching the full user row height.
|
||||||
divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16));
|
divider->setSizeHint(0, QSize(0, QFontMetrics(font).height() + 16));
|
||||||
|
|
||||||
userTree->addTopLevelItem(divider);
|
userTree->addTopLevelItem(divider);
|
||||||
// QTreeWidgetItem::setFirstColumnSpanned() is a no-op while the item is
|
// QTreeWidgetItem::setFirstColumnSpanned() does nothing while the item is
|
||||||
// detached from the tree (Qt returns early when treeModel() is null), so it
|
// detached from the tree (Qt returns early when treeModel() is null), so it
|
||||||
// must be called after addTopLevelItem(). Without the span the divider text
|
// must be called after addTopLevelItem(). Without the span the divider text
|
||||||
// is confined to column 0 and gets elided in unstyled mode.
|
// is confined to column 0 and gets elided in unstyled mode.
|
||||||
|
|
@ -1670,7 +1669,7 @@ void UserListWidget::handleOnlineChange(const ServerInfo_User &user)
|
||||||
void UserListWidget::handleOnlineChangeLeft(const QString &userName)
|
void UserListWidget::handleOnlineChangeLeft(const QString &userName)
|
||||||
{
|
{
|
||||||
// The user is no longer online: their "Online" row disappears. Buddies and
|
// The user is no longer online: their "Online" row disappears. Buddies and
|
||||||
// ignored users keep their own section's row, marked offline; a plain user
|
// ignored users keep their own section's row, marked offline. A plain user
|
||||||
// has no rows left.
|
// has no rows left.
|
||||||
const bool dropped = dropSectionMembership(QStringLiteral("online"), userName);
|
const bool dropped = dropSectionMembership(QStringLiteral("online"), userName);
|
||||||
const bool kept = manager->isUserBuddy(userName) || manager->isUserIgnored(userName);
|
const bool kept = manager->isUserBuddy(userName) || manager->isUserIgnored(userName);
|
||||||
|
|
@ -1688,7 +1687,7 @@ void UserListWidget::handleListAdd(const QString §ionId, const ServerInfo_Us
|
||||||
const bool online = manager->getOnlineUser(name) != nullptr;
|
const bool online = manager->getOnlineUser(name) != nullptr;
|
||||||
ensureSectionMembership(sectionId, user, online);
|
ensureSectionMembership(sectionId, user, online);
|
||||||
if (online) {
|
if (online) {
|
||||||
// The user belongs to the "Online" section as well; make sure the row
|
// The user belongs to the "Online" section as well. Make sure the row
|
||||||
// exists even if the join event raced ahead of the list mutation.
|
// exists even if the join event raced ahead of the list mutation.
|
||||||
ensureSectionMembership(QStringLiteral("online"), user, true);
|
ensureSectionMembership(QStringLiteral("online"), user, true);
|
||||||
}
|
}
|
||||||
|
|
@ -1722,10 +1721,10 @@ UserListTWI *UserListWidget::ensureSectionMembership(const QString §ionId, c
|
||||||
sectionMap.insert(userName, item);
|
sectionMap.insert(userName, item);
|
||||||
divider->addChild(item);
|
divider->addChild(item);
|
||||||
if (!users.contains(userName)) {
|
if (!users.contains(userName)) {
|
||||||
users.insert(userName, item); // primary row for name-based lookups
|
users.insert(userName, item); // primary row for lookups by name
|
||||||
}
|
}
|
||||||
// The divider counts are refreshed once in endBulkLoad(); per-row
|
// The divider counts are refreshed once in endBulkLoad(). Calling
|
||||||
// updateCount() during a large rebuild would be quadratic.
|
// updateCount() per row during a large rebuild would be quadratic.
|
||||||
if (!bulkLoading) {
|
if (!bulkLoading) {
|
||||||
updateCount(); // a new row changes the divider's count
|
updateCount(); // a new row changes the divider's count
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,9 +165,9 @@ private:
|
||||||
bool bulkLoading = false;
|
bool bulkLoading = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Popup functions are anchored on the row, not the user name: in sectioned
|
* Popup functions are anchored on the row, not the user name. In sectioned
|
||||||
* mode a user can own several rows (online + buddy), and the popup must
|
* mode a user can own several rows (online + buddy), and the popup must
|
||||||
* follow the hovered/selected row rather than a name-based lookup.
|
* follow the hovered/selected row rather than a lookup by name.
|
||||||
*/
|
*/
|
||||||
void showPopupForUser(UserListTWI *item);
|
void showPopupForUser(UserListTWI *item);
|
||||||
void hidePopup(bool immediate = false);
|
void hidePopup(bool immediate = false);
|
||||||
|
|
@ -176,7 +176,7 @@ private:
|
||||||
bool isItemNearViewport(const UserListTWI *item) const;
|
bool isItemNearViewport(const UserListTWI *item) const;
|
||||||
void requestAvatarsForVisibleItems();
|
void requestAvatarsForVisibleItems();
|
||||||
|
|
||||||
// ── Sectioned mode (single tree with inline dividers) ────────────────────
|
// Sectioned mode (single tree with inline dividers)
|
||||||
bool sectioned = false;
|
bool sectioned = false;
|
||||||
QStringList sectionIds;
|
QStringList sectionIds;
|
||||||
QMap<QString, QTreeWidgetItem *> sectionItems;
|
QMap<QString, QTreeWidgetItem *> sectionItems;
|
||||||
|
|
@ -199,7 +199,7 @@ private:
|
||||||
UserListTWI *ensureSectionMembership(const QString §ionId, const ServerInfo_User &user, bool online);
|
UserListTWI *ensureSectionMembership(const QString §ionId, const ServerInfo_User &user, bool online);
|
||||||
/** Removes and deletes the row for @p userName in @p sectionId. */
|
/** Removes and deletes the row for @p userName in @p sectionId. */
|
||||||
bool dropSectionMembership(const QString §ionId, const QString &userName);
|
bool dropSectionMembership(const QString §ionId, const QString &userName);
|
||||||
/** Sorts, re-filters and repaints after a sectioned-mode mutation. */
|
/** Sorts, refilters and repaints after a sectioned mode mutation. */
|
||||||
void finishSectionedMutation();
|
void finishSectionedMutation();
|
||||||
void updateCardArtParams(const ServerInfo_User &user, const QString &userName);
|
void updateCardArtParams(const ServerInfo_User &user, const QString &userName);
|
||||||
void processUserInfo(const QString §ionId, const ServerInfo_User &user, bool online);
|
void processUserInfo(const QString §ionId, const ServerInfo_User &user, bool online);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue