Files
utun2/tools/chatgui/src/memberlistmodel.h
T

111 lines
8.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Плоская Qt-модель участников одного канала, только GUI-поток с QApplication.
* Хранит полные снимки CHAT_EVT_MEMBER_*, без чтения живых структур ядра/БД.
* Последовательный массив совпадает с порядком строк; member(nodeId) возвращает независимую копию.
* Полный снимок проверяется/готовится до reset, обновление одной записи заменяет её целиком.
* Без isOwner удалённые записи скрыты, смена канала очищает снимки и эфемерную речь.
* Запросы идут через owned bridge API: остановка ядра отменяет их и освобождает аргументы. */
#ifndef MEMBERLISTMODEL_H
#define MEMBERLISTMODEL_H
#include <QAbstractListModel>
#include <QVector>
#include <QSet>
#include <QString>
#include <QPixmap>
#include <QTimer>
#include <stdint.h>
#include <optional>
#include "../../../src/chat/chat_member.h"
struct MemberListItem {
chat_member_display snapshot{}; // полная запись выбранного канала; источник всех live-полей
QPixmap avatar; // производное изображение строки
QString name; // UTF-8 имя из snapshot для Qt
};
class MemberListModel : public QAbstractListModel {
Q_OBJECT
public:
/* Высота строки 36 px, между аватарками остаётся 2 px. */
static constexpr int kRowHeight = 36;
static constexpr int kAvatarSize = kRowHeight - 2;
static constexpr int RadioSpeakingRole = Qt::UserRole + 5;
static constexpr int OnlineRole = Qt::UserRole + 6; /* живой статус BGP для кнопок вызова */
static constexpr int NodeIdRole = Qt::UserRole + 9; // цель действий для обычной строки и её proxy
static constexpr int DevgroupRole = Qt::UserRole + 12; // ключ devgroup в hex, пусто без группы
static constexpr int AvatarNodeIdRole = Qt::UserRole + 13; // владелец показываемого аватара
explicit MemberListModel(QObject* parent = nullptr); // parent владеет; собственный timer опрашивает RTT каждые 2 с
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
/* channelId — строковый ID до 63 байт UTF-8, пустой очищает модель. Смена owner-политики
* скрывает удалённых сразу; полный refresh асинхронный, при недоступном ядре сохраняет снимок. */
void setChannel(const QString& channelId, bool isOwner);
void refresh(); // запрос полного списка, без очистки показанных строк
void refreshRtt(); // запрос полных MEMBER_UPDATED, включая все live-поля, не только RTT
quint64 nodeIdAt(int row) const; // 0 вне диапазона
bool isOnlineAt(int row) const; // false вне диапазона; online из последнего снимка, не из БД
/* Копия записи: указатели в row-cache нельзя сохранять через reset/обновления. */
std::optional<chat_member_display> member(quint64 nodeId) const;
/* Единый формат фактического подключения для строки и деталей. Bn/BGP(n hop) считает посредников.
* detailed добавляет пару NAT «наш узел — участник» для DIR; неизвестный тип обозначен ?, RTT не округляется до целых. */
QString routeText(const chat_member_display& member, bool detailed) const;
/* GUI callbacks, data заимствован на время вызова. Другой chId игнорируется.
* List требует ровно count*CHAT_MEMBER_DISPLAY_SIZE байтов; (0,NULL,0) очищает список.
* Updated требует доступный полный CHAT_MEMBER_DISPLAY_SIZE: длину проверяет caller/bridge codec.
* NULL/ошибка списка сохраняют прежнюю модель; неизвестный nodeId добавляется в конец.
* Removed сдвигает следующие строки с корректными Qt-уведомлениями; отсутствие допустимо. */
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
void onMemberUpdated(const QString& chId, const uint8_t* data);
void onMemberRemoved(const QString& chId, uint64_t nodeId);
/* Эфемерное состояние рации; вызывается в GUI-потоке, независимо от обновлений строк. */
void setRadioState(quint64 groupId, bool active, bool transmitting);
void onRadioTalk(quint64 groupId, quint64 nodeId, bool on);
signals:
/* nodeId=0: полный снимок/смена канала; иначе изменился или удалён конкретный участник. */
void snapshotChanged(quint64 nodeId);
private:
QString m_channelId; // выбранный канал, публикуется до modelReset/snapshotChanged
bool m_isOwner = false; // разрешает показывать удалённых участников
QVector<MemberListItem> m_members; // собственные снимки, индекс массива == номер строки
QSet<quint64> m_radioTalkers; // START может прийти до загрузки строки; STOP/stop radio/смена канала очищают
bool m_radioActive = false; // приём START только для активной рации выбранного канала
bool m_selfTransmitting = false; // PTT/VAD из аудио-движка, не состояние удалённых источников
QTimer* m_rttTimer; // QObject-child, останавливается при уничтожении модели
};
/* Представление поверх снимков: заголовки не являются устройствами; аватар берётся у первого
* участника группы, кнопки действуют на первое online-устройство. RADIO/PHONE-флаги заголовка
* объединяют состояния всех неудалённых участников; mute/self относятся к цели кнопок.
* Self/его группа всегда первые, в раскрытой группе self тоже первый; порядок остальных сохраняется. Источник заимствован. */
class GroupedMemberModel : public QAbstractListModel {
Q_OBJECT
public:
static constexpr int GroupHeaderRole = Qt::UserRole + 7;
static constexpr int GroupExpandedRole = Qt::UserRole + 8;
static constexpr int ActionNodeIdRole = Qt::UserRole + 9; // конкретное устройство для кнопок; 0 если в группе нет online
static constexpr int GroupBranchRole = Qt::UserRole + 10; // 0 — без ветки; 1 — продолжение; 2 — последнее устройство
static constexpr int CallAvailableRole = Qt::UserRole + 11; // Есть доступный адресат кроме локального узла.
static constexpr int DisclosureWidth = 24; // одинаковая левая колонка во всех строках
explicit GroupedMemberModel(MemberListModel* source, QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = {}) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
int sourceRow(int row) const; // -1 для заголовка/неверной строки
int actionSourceRow(int row) const; // заголовок: первое online-устройство, -1 если их нет
int avatarSourceRow(int row) const; // заголовок: первое устройство независимо от online
bool toggle(int row); // true — обработан заголовок
private:
void rebuild(bool forceReset = false);
MemberListModel* m_source;
QVector<int> m_rows; // >=0 — строка источника, <0 — заголовок группы строки -value-1
QSet<QByteArray> m_expanded;
};
#endif