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

165 lines
19 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.
/* GUI-composer канала/PM: текст, emoji, ответ/редактирование, голосовая запись и preview одного вложения.
* Сам не отправляет данные в сеть: владелец обрабатывает сигналы в GUI и передаёт работу ядру.
* Для submitAttachment нужен ровно один обработчик, принимающий request вместе с PCM/compressor даже при отказе.
* Канальный текст/файлы/ответы/правки ждут submissionResult; PM-вложения очищаются сразу после сигнала отправки.
* emojiPanel/recorder заимствованы, могут быть nullptr и должны жить во время использования; остальные виджеты — дочерние. */
#pragma once
#include <QWidget>
#include <QHash>
#include <QSet>
#include <QPointer>
class QTextEdit;
class QPushButton;
class QCheckBox;
class QLabel;
class QProgressBar;
class QTimer;
class EmojiPanel;
class AudioRecorder;
struct attachment_send_req;
class InputBar : public QWidget {
Q_OBJECT
public:
/* Создать пустой composer; parent владеет виджетом. Перед отправкой задать цель через setRecordingContext.
* Для PTT recorder должен быть init; его жизненным циклом и остановкой при закрытии владеет вызывающий. */
explicit InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder,
QWidget *parent = nullptr);
~InputBar() override; // Снимает drop-фильтры/callbacks, удаляет drag-подсказку; не уничтожает заимствованные объекты.
/* mediaDirBase — каталог media; channelId — канал либо dm:<conv_id>, dmGeneration — поколение PM (для канала 0).
* Изменение контекста отменяет edit/reply/preview и отложенный drop; обычный текст сохраняется.
* Запись уже фиксирует прежний target/generation; удаление текущего поколения PM отдельно очищает его черновик. */
void setRecordingContext(const QString& mediaDirBase, const QString& channelId, quint64 dmGeneration = 0);
/* Очистить текст/reply/preview и остановить запись только при совпадении target/generation.
* Известные задачи target исключаются из индикатора; их поздние завершения игнорируются. Другой выбранный адресат не меняется. */
void forgetDmHistory(const QString& target, quint64 generation);
/* Заимствовать target, включить acceptDrops и установить общий фильтр URL-drop; повтор/null — no-op.
* Уничтожение target снимает регистрацию, destructor composer — фильтр. acceptDrops обратно не переключается.
* Drop только готовит preview; отправку инициируют кнопка/Enter. */
void installFileDropTarget(QWidget* target);
/* Режим PM скрывает/снимает групповое шифрование вложения; метод также разрешает PTT/скрепку.
* Ограничение media/edit нужно применять после него через setMediaEnabled. */
void setDmMode(bool enabled);
void setMediaEnabled(bool enabled); // Разрешить media-кнопки с учётом edit; false не останавливает уже идущую запись.
/* Состояние подготовки job в target: 0=start, 1=готово, 2=ошибка; job — непрозрачный UUID задачи.
* Кеширует и для невыбранных целей; индикатор показывает только текущую, активные задачи имеют приоритет над ошибкой. */
void updatePreparation(const QString& target, const QByteArray& job, int state);
/* Выбрать ответ в канале: messageId ровно 32 байта, author/preview — plain text, пробелы в подписи упрощаются.
* PM/пустая цель/неверный ключ отклоняются; edit сначала отменяется. Новый ответ имеет собственное поколение. */
void setReply(const QByteArray& messageId, const QString& author, const QString& preview);
void clearReply(); // Снять ответ и сменить поколение; текст/preview вложения не очищаются, старые результаты не применяются.
/* Подтвердить канальный текст/файл, правку либо одну часть ответа по request; 0/неизвестный ID игнорируется.
* Ошибка сохраняет черновик и показывает отказ. Успех очищает лишь неизменённую ревизию текста/вложения;
* ответ снимается после успеха всех частей и очистки черновика. Старое поколение/другая цель не затрагиваются. */
void submissionResult(quint64 request, bool success);
/* Открыть edit канального сообщения: id=32 байта, version>0, text — исходный текст.
* Сохраняет обычный черновик/reply, скрывает вложение; PM, pending edit/reply или PTT дают отказ с логом. */
void editMessage(const QByteArray& id, quint64 version, const QString& text);
void cancelEdit(); // Восстановить обычный черновик/reply/preview; late result edit игнорируется, запрос ядра не отменяется.
signals:
/* Текст текущего контекста; справа удалены только пробелы/табуляции. replyTo пуст либо ключ 32 байта.
* request ненулевой для канала (включая обычный текст), 0 для PM; обработчик фиксирует текущую цель синхронно. */
void sendMessage(const QString &text, const QByteArray& replyTo, quint64 request);
/* Снимок правки: channel/id/version/text и ненулевой request; ввод read-only до результата/отмены. */
void saveEdit(const QString& channel, const QByteArray& id, quint64 version, const QString& text, quint64 request);
/* Передать единственному GUI-обработчику собственный u_calloc-request голоса/видео/зашифрованного файла.
* target зафиксирован (для PTT при старте); обработчик заполняет inst/target/is_dm и передаёт владение ядру
* либо освобождает request/PCM/compressor. После сигнала InputBar не использует request. */
void submitAttachment(const QString& target, struct attachment_send_req* request);
/* Файл без подготовки видео: source не копируется здесь; dest — запланированный путь, в PM совпадает с source.
* target/name/reply/request относятся к выбранному preview. Для канала нужен submissionResult; для PM request=0. */
void sendFileAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName, const QByteArray& replyTo, quint64 request);
/* Изображение канала; аргументы/подтверждение как у file. В PM картинки идут обычным file-сигналом. */
void sendImageAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName, const QByteArray& replyTo, quint64 request);
void attachmentRejected(const QString& reason); // Причина отказа для пользователя; подключён внутренний показ QToolTip.
protected:
bool eventFilter(QObject *obj, QEvent *event) override; // URL-drop; Enter отправляет, Ctrl+Enter вставляет LF, Escape снимает edit/reply.
void resizeEvent(QResizeEvent* event) override; // Выравнивает overlay записи по геометрии текстового поля.
private slots:
void onSendClicked(); // Edit либо вложение и текст отдельными сигналами; повтор текущей pending отправки блокируется.
void adjustInputHeight(); // От 1 до 12 строк, дальше включает вертикальную прокрутку с сохранением видимости курсора.
void onPttPressed(); // Запускает recorder и фиксирует target/generation/reply только при успешном старте.
void onPttReleased(); // Stop; запись от 500 мс передаётся через takeRecording/submitAttachment, меньшая отбрасывается.
void onAttachClicked(); // QFileDialog → та же подготовка preview, что у drop; контекст проверяется после модального выбора.
void clearPendingAttachment(); // Снять preview/encrypt, увеличить ревизию; исходный файл не удаляется.
void updateRecordingUI(int ms); // Длительность и RMS recorder (-70..0 dB → 0..100), чтение peakLevel потребляет метрику.
void showRecordOverlay(); // Показать overlay и начальную длительность/уровень по recordingStarted.
void hideRecordOverlay(); // Скрыть по recordingStopped; не выполняет отправку записи.
private:
static QString formatFileSize(qint64 bytes); // Человекочитаемые B/KB/MB/GB, шаг 1024; bytes ожидается неотрицательным.
struct PendingSubmission {
quint64 generation; // Ревизия выбора ответа/контекста на момент отправки этой части.
QString target, text, source; // Собственные снимки цели и отправленного текста/исходника; одна из частей может быть пустой.
int textRevision; // QTextDocument::revision для защиты нового/изменённого черновика от позднего success.
quint64 attachmentRevision; // Ревизия preview для такой же защиты выбранного файла.
};
/* Запомнить канальный текст/файл либо часть ответа; ID общий для всех composer, чтобы late result не попал новой форме.
* Вложение без source/reply и файл PM не требуют подтверждения: вернуть 0 и ничего не запоминать. */
quint64 beginSubmission(const QByteArray& reply, quint64 generation, const QString& target,
const QString& text = {}, const QString& source = {});
bool submissionPending() const; // Есть ожидающая часть текущего контекста/выбора ответа.
QByteArray m_editId, m_savedReply; // Ключ редактируемого сообщения и ответ обычного черновика, сохранённый на время edit.
QString m_savedText, m_savedReplyLabel; // Обычный текст/подпись ответа для cancelEdit или успешного сохранения.
quint64 m_editVersion = 0, m_editRequest = 0; // Ожидаемая версия и ненулевой ID pending save; 0 — нет запроса.
QPushButton* m_sendBtn = nullptr; // Send либо Save edit, действие определяется m_editId.
QWidget* m_replyPreview = nullptr; // Общий preview ответа или режима edit.
QLabel* m_replyLabel = nullptr; // Plain text автора/цитаты либо подписи edit.
QByteArray m_replyTo, m_recordReply; // Выбранный ответ и его снимок для начатой голосовой записи.
quint64 m_replyGeneration = 0, m_recordReplyGeneration = 0; // Поколения текущего выбора и ответа голосовой записи.
static quint64 s_submissionSequence; // GUI-only: общий для всех InputBar счётчик request ID, включая edit.
bool m_submissionFailed = false; // Хотя бы одна часть получила отказ; ответ не снимается при успехе другой части.
quint64 m_attachmentRevision = 0; // Меняется при замене/очистке preview, не зависит от совпадения пути файла.
QHash<quint64, PendingSubmission> m_pendingSubmissions; // ID → ожидающая часть; чужие поколения удаляются при получении результата.
QTextEdit *m_input; // Plain-text поле и focusProxy composer.
QPushButton *m_emojiBtn; // Переключает видимость заимствованной панели, если она есть.
QPushButton *m_pttBtn; // Press/start, release/stop, красный фон при активной записи.
QPushButton *m_attachBtn = nullptr; // Открывает выбор одного локального файла.
QCheckBox *m_encryptCheck = nullptr; // Только канал; сбрасывается при смене/очистке preview и переходе в PM.
EmojiPanel *m_emojiPanel; // Заимствована; emojiSelected вставляется в текущую позицию курсора.
AudioRecorder *m_recorder; // Заимствован, nullptr отключает запуск PTT.
QWidget *m_recordOverlay = nullptr; // Дочерний overlay над m_input.
QLabel *m_recordLabel = nullptr; // Время записи M:SS.
QPushButton *m_cancelRecBtn = nullptr; // Stop без отправки: снимает m_pttActive до остановки recorder.
QProgressBar *m_levelBar = nullptr; // RMS последнего обработанного блока recorder, 0..100.
QWidget *m_attachPreview = nullptr; // Preview выбранного файла, скрыт до успешной подготовки выбора.
QLabel *m_attachPreviewLabel = nullptr; // Имя/размер файла; его содержимое здесь не читается.
QPushButton *m_attachClearBtn = nullptr; // Снять preview без удаления исходника.
bool m_pttActive = false; // Composer начал запись, которую release ещё может отправить.
bool m_dmMode = false; // PM: изображения идут файлами, групповое шифрование скрыто.
bool m_mediaEnabled = true; // Запрошенное разрешение media, используется при восстановлении кнопок после edit.
quint64 m_dmGeneration = 0, m_recordDmGeneration = 0; /* Поколения текущего composer и начатой записи PM. */
QString m_mediaDirBase; // База планируемых путей вложений; файлы PM без transcode остаются по исходному пути.
QString m_channelIdForRecord; // Текущий target composer: канал либо dm:<id>.
QString m_recordTarget; // Target, зафиксированный при старте PTT, независимо от дальнейшего выбора канала.
QString m_pendingAttachSrc; // Локальный source preview, не копируется/удаляется composer.
QString m_pendingAttachDest; // Планируемый mediaDirBase/target/имя либо исходный путь PM.
QString m_pendingAttachName; // Исходное имя для отображения/метаданных; видео при отправке получает суффикс mp4.
qint64 m_pendingAttachSize = 0; // Размер на момент выбора, bytes.
bool m_pendingAttachAsVideo = false; // Пользователь выбрал подготовку/transcode видео.
bool m_pendingAttachAsImage = false; // Расширение картинки в канале; PM не использует этот режим.
QLabel* m_preparationLabel = nullptr; // Статус подготовки текущего target.
QHash<QString, QSet<QByteArray>> m_preparations; // Target → UUID активных задач ядра.
QSet<QString> m_preparationErrors; // Target с последней ошибкой, снимается при новом start или forget.
QSet<QByteArray> m_discardedPreparations; /* Завершения задач удалённой истории, ещё ожидаемые из ядра. */
void showPreparation(); // Обновить индикатор для текущей цели по кешу задач/ошибок.
bool canAttachFile() const; // Enabled, выбран target, скрепка разрешена, нет edit/PTT/записи recorder.
/* Проверить source/context, выбрать режим видео и подготовить preview; true — выбор принят, false — отмена/отказ.
* Старый preview заменяется лишь после успеха; source остаётся неизменным, для групп/видео создаётся каталог назначения. */
bool prepareAttachment(const QString& source, quint64 context);
void rejectAttachment(const QString& reason, const QString& source = QString()); // Лог с target/source и пользовательский сигнал.
QSet<QObject*> m_dropTargets; // Заимствованные виджеты с фильтром, включая self и viewport; destroyed удаляет внешние записи.
QPointer<QLabel> m_dropHint; // Собственная drag-подсказка, QObject-parent — текущий drop target; может исчезнуть вместе с ним.
quint64 m_attachmentContext = 0; // Ревизия контекста, проверяемая до/после модального выбора и в queued drop.
};