Cockatrice/cockatrice/src/interface/widgets/tabs/tab_room.h
BruebachL d99798111e
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
[UserList] Unify friends/online/ignored list with section dividers and add search bar. (#7119)
* [UserList] Unify friends/online/ignored list with section dividers and add search bar.

Took 31 minutes

Took 7 seconds

* [UserList] Add a light mode theme

Took 12 minutes

Took 11 seconds


Took 5 minutes

Took 2 minutes

* [UserList] Re-sort when a user's online state changes

setUserOnline() flipped the online flag but never re-sorted, so a buddy
who went offline kept the position they had while online and stayed at
the top of the list. Re-sort (and re-apply the filter) whenever the flag
actually changes, mirroring processUserInfo().

Took 10 minutes

* [UserList] Show users in every section they belong to

The sectioned list used one row per user with a priority rule
(ignored > buddy > online), so an online buddy only appeared under
"Buddies" and never in the "Online" list. Sections are now pure
membership views: a user gets one row per section they belong to, so an
online buddy appears under both "Online" and "Buddies".

- Track rows per (section, user) in sectionUsers instead of reparenting
  a single row; the name->primary-row map is kept for external lookups.
- Rebuild, presence and buddy/ignore mutations create/drop rows per
  section instead of moving a single row between sections.
- Dropping one membership no longer removes the user from the other
  sections.

* [UserList] Keyboard navigation for section dividers, popup on selection

Section dividers were not selectable, so arrow-key navigation skipped
them entirely, and the user popup only appeared on hover or click. Now:

- Dividers are selectable, so Up/Down navigation lands on them; they act
  as collapsible headers once focused (Enter/Space toggle, Left/Right
  collapse/expand per tree convention), with a focus indicator drawn by
  the existing delegate.
- The popup follows keyboard selection via currentItemChanged, exactly
  like mouse hover, and closes when the selection moves to a divider or
  leaves the list.
- The popup anchors on the hovered/selected row instead of a user-name
  lookup, so with duplicate rows (online + buddy) it stays attached to
  the row under the mouse/cursor.
- Left-arrow now actually collapses an expanded section divider: the
  collapse branch hardcoded the target expansion state to 'expanded',
  making the key a no-op.
- The user popup no longer flashes through a fade when hopping between
  users (hover or arrow-key navigation): a content swap keeps it opaque,
  and pending show/hide timers are cancelled so an armed hover timer
  cannot override a keyboard-selected row or a pending hide kill the
  newly shown popup.
- Bulk rebuild defers per-row divider-count updates to endBulkLoad(),
  removing the quadratic recount during large online-list loads.
- handleOnlineChangeLeft/handleListRemove skip the sort+filter+repaint
  when nothing actually changed.

* [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.

* [UserList] Deselect the list and close the popup on outside clicks

Clicking anywhere outside the tree, the popup or an open menu now clears
the selection and hides the popup, so a pinned popup does not stay open
when the list loses focus.

- The application-wide event filter watches every mouse press and treats
  a press as inside the list UI only when its target is the tree, the
  popup or an open menu (parent-chain walk), so a click on another list,
  a tab or the window background deselects.
- A hover popup now also closes when the cursor leaves the hovered row.
  The hide timer previously checked whether the cursor was over the
  tree, which is always true over empty list space and section dividers,
  so the popup stayed open after moving off the user.
- Deselection keeps the current item so keyboard navigation is not
  disturbed, and the pinned flag is dropped before hiding so the
  selection-changed handler does not hide twice.

Took 15 minutes

* [UserList] Use an enum for the list sections

The section identifiers were stringly-typed: eleven hardcoded
QStringLiteral comparisons scattered through user_list_widget.cpp, and
the display path (sectionTitle) maps every id through tr() anyway, so
the raw strings were never shown. A typo compiled fine and silently
broke a section.

- enum class Section { Buddy, Online, Ignore } replaces the section
  strings across the sectioned-list API (setSectioned, getSectionIds,
  setSectionExpanded, the sectionExpanded signal and all membership
  helpers), giving compile-time checks at every call site.
- sectionTitle becomes a switch over the enum and the dead raw-string
  fallback is gone.
- The expanded-section state persists the same stable keys via the
  panel widget boundary, so existing settings files survive unchanged.
- The divider reverse lookup in handleSectionExpansion no longer relies
  on an empty-string sentinel from QMap::key; it scans the three
  dividers and bails when the item is not one of them.

Took 12 minutes

# Commit time for manual adjustment:
# Took 2 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-15 22:53:13 +02:00

146 lines
3.5 KiB
C++

/**
* @file tab_room.h
* @ingroup RoomTabs
* @ingroup Lobby
*/
//! \todo Document this file.
#ifndef TAB_ROOM_H
#define TAB_ROOM_H
#include "../interface/widgets/utility/line_edit_completer.h"
#include "tab.h"
#include <QFocusEvent>
#include <QGroupBox>
#include <QMap>
#include <QStringListModel>
class UserListProxy;
class UserListManager;
namespace google
{
namespace protobuf
{
class Message;
}
} // namespace google
class AbstractClient;
class UserListWidget;
class UserListPanelWidget;
class QLabel;
class ChatView;
class QPushButton;
class QTextTable;
class QCompleter;
class RoomEvent;
class ServerInfo_Room;
class ServerInfo_Game;
class Event_ListGames;
class Event_JoinRoom;
class Event_LeaveRoom;
class Event_RoomSay;
class Event_RemoveMessages;
class GameSelector;
class Response;
class PendingCommand;
class ServerInfo_User;
class LineEditCompleter;
class TabRoom : public Tab
{
Q_OBJECT
private:
AbstractClient *client;
int roomId;
QString roomName;
ServerInfo_User *ownUser;
QMap<int, QString> gameTypes;
GameSelector *gameSelector;
UserListPanelWidget *userListPanel;
UserListWidget *userList;
const UserListProxy *userListProxy;
ChatView *chatView;
QLabel *sayLabel;
LineEditCompleter *sayEdit;
QStringListModel *mentionModel;
QGroupBox *chatGroupBox;
QMenu *roomMenu;
QAction *aLeaveRoom;
QAction *aOpenChatSettings;
QAction *aClearChat;
[[nodiscard]] QString sanitizeHtml(QString dirty) const;
QStringList autocompleteUserList;
QCompleter *mentionCompleter;
signals:
void roomClosing(TabRoom *tab);
void openMessageDialog(const QString &userName, bool focus);
void maximizeClient();
void notIdle();
void gameListUpdated();
private slots:
void sendMessage();
void sayFinished(const Response &response);
void actClearChat();
void actOpenChatSettings();
void addMentionTag(QString mentionTag);
void focusTab();
void actShowMentionPopup(const QString &sender);
void actShowPopup(const QString &message);
void actCompleterChanged();
void processListGamesEvent(const Event_ListGames &event);
void processJoinRoomEvent(const Event_JoinRoom &event);
void processLeaveRoomEvent(const Event_LeaveRoom &event);
void processRoomSayEvent(const Event_RoomSay &event);
void processRemoveMessagesEvent(const Event_RemoveMessages &event);
void refreshShortcuts();
protected slots:
void closeEvent(QCloseEvent *event) override;
public:
TabRoom(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
ServerInfo_User *_ownUser,
const ServerInfo_Room &info);
void retranslateUi() override;
void tabActivated() override;
void processRoomEvent(const RoomEvent &event);
[[nodiscard]] int getRoomId() const
{
return roomId;
}
[[nodiscard]] QString getRoomName() const
{
return roomName;
}
[[nodiscard]] const QMap<int, QString> &getGameTypes() const
{
return gameTypes;
}
[[nodiscard]] QString getChannelName() const
{
return roomName;
}
[[nodiscard]] QString getTabText() const override
{
return roomName;
}
[[nodiscard]] const ServerInfo_User *getUserInfo() const
{
return ownUser;
}
[[nodiscard]] GameSelector *getGameSelector() const
{
return gameSelector;
}
PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd);
void sendRoomCommand(PendingCommand *pend);
};
#endif