Browse Source

chatgui: stage dropped files through the attachment composer

master
evgeny 6 days ago
parent
commit
0279ed84a9
  1. 150
      tools/chatgui/src/inputbar.cpp
  2. 10
      tools/chatgui/src/inputbar.h
  3. 1
      tools/chatgui/src/messagelist.cpp

150
tools/chatgui/src/inputbar.cpp

@ -23,6 +23,11 @@
#include <QIcon>
#include <QTimer>
#include <QFileInfo>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QUrl>
#include <QToolTip>
#include <cmath>
extern "C" {
@ -37,6 +42,10 @@ static bool isImageFile(const QString& suffix) {
s == "bmp" || s == "webp";
}
static bool isVideoFile(const QString& suffix) {
return QStringList{"mp4", "mov", "mkv", "avi", "webm", "m4v"}.contains(suffix.toLower());
}
static QPushButton *makeIconBtn(const QIcon& icon, const QString &tooltip, QWidget *parent) {
auto *btn = new QPushButton(parent);
btn->setIcon(icon);
@ -198,9 +207,81 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par
}
mainLayout->addLayout(layout);
installFileDropTarget(this);
installFileDropTarget(m_input->viewport());
connect(this, &InputBar::attachmentRejected, this, [this](const QString& reason) {
QToolTip::showText(mapToGlobal(QPoint(8, 0)), reason, this);
});
}
/* Qt доставляет drop в viewport, а не в QTextEdit/QListView. Родитель composer принимает drop на кнопках. */
void InputBar::installFileDropTarget(QWidget* target) {
if (!target || m_dropTargets.contains(target)) return;
target->setAcceptDrops(true);
target->installEventFilter(this);
m_dropTargets.insert(target);
connect(target, &QObject::destroyed, this, [this, target] { m_dropTargets.remove(target); });
GUI_DEBUG("InputBar: file drop target installed class=%s", target->metaObject()->className());
}
bool InputBar::canAttachFile() const {
return !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() &&
!m_pttActive && (!m_recorder || !m_recorder->isRecording());
}
void InputBar::rejectAttachment(const QString& reason, const QString& source) {
GUI_WARN("attachment rejected: target=%s source=%s reason=%s", qPrintable(m_channelIdForRecord),
qPrintable(source), qPrintable(reason));
emit attachmentRejected(reason);
}
bool InputBar::eventFilter(QObject *obj, QEvent *event) {
if (m_dropTargets.contains(obj)) {
if (event->type() == QEvent::DragLeave) {
if (m_dropHint) m_dropHint->hide();
} else if (event->type() == QEvent::DragEnter || event->type() == QEvent::DragMove || event->type() == QEvent::Drop) {
auto* drop = static_cast<QDropEvent*>(event);
if (!drop->mimeData()->hasUrls()) return QWidget::eventFilter(obj, event);
if (!canAttachFile() || !(drop->possibleActions() & Qt::CopyAction)) {
if (m_dropHint) m_dropHint->hide();
drop->ignore();
if (event->type() == QEvent::DragEnter)
GUI_DEBUG("attachment drag unavailable: target=%s enabled=%d recording=%d actions=%d",
qPrintable(m_channelIdForRecord), m_attachBtn->isEnabled(), m_pttActive, int(drop->possibleActions()));
return true;
}
const auto urls = drop->mimeData()->urls();
QString error;
if (urls.size() != 1) error = QCoreApplication::translate("ChatGui", "Drop one file at a time.");
else if (!urls.front().isLocalFile()) error = QCoreApplication::translate("ChatGui", "Only local files can be attached.");
if (event->type() != QEvent::Drop) {
auto* target = qobject_cast<QWidget*>(obj);
if (!m_dropHint) {
m_dropHint = new QLabel(target);
m_dropHint->setAttribute(Qt::WA_TransparentForMouseEvents);
m_dropHint->setAlignment(Qt::AlignCenter);
m_dropHint->setWordWrap(true);
m_dropHint->setStyleSheet("QLabel { border: 2px dashed palette(highlight); color: palette(text);"
" background: rgba(40, 110, 160, 80); }");
}
if (m_dropHint->parentWidget() != target) m_dropHint->setParent(target);
m_dropHint->setGeometry(target->rect().adjusted(2, 2, -2, -2));
m_dropHint->setText(error.isEmpty() ? QCoreApplication::translate("ChatGui", "Drop a file to attach") : error);
m_dropHint->show(); m_dropHint->raise();
drop->setDropAction(Qt::CopyAction); drop->accept();
return true;
}
if (m_dropHint) m_dropHint->hide();
if (!error.isEmpty()) { rejectAttachment(error); drop->ignore(); return true; }
const QString source = urls.front().toLocalFile();
const quint64 context = m_attachmentContext;
GUI_INFO("attachment dropped: target=%s source=%s", qPrintable(m_channelIdForRecord), qPrintable(source));
// Завершаем native drag до возможного модального выбора режима видео; контекст проверяется повторно.
QTimer::singleShot(0, this, [this, source, context] { prepareAttachment(source, context); });
drop->setDropAction(Qt::CopyAction); drop->accept();
return true;
}
}
if (obj == m_input && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
@ -283,7 +364,11 @@ void InputBar::onSendClicked() {
}
void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId) {
if (m_channelIdForRecord != channelId) clearPendingAttachment();
if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) {
++m_attachmentContext;
clearPendingAttachment();
if (m_dropHint) m_dropHint->hide();
}
m_mediaDirBase = mediaDirBase;
m_channelIdForRecord = channelId;
showPreparation();
@ -298,6 +383,7 @@ void InputBar::setDmMode(bool enabled) {
void InputBar::setMediaEnabled(bool enabled) {
if (m_pttBtn) m_pttBtn->setEnabled(enabled);
if (m_attachBtn) m_attachBtn->setEnabled(enabled);
if (!enabled && m_dropHint) m_dropHint->hide();
}
void InputBar::onPttPressed() {
@ -331,35 +417,30 @@ void InputBar::onPttReleased() {
}
void InputBar::onAttachClicked() {
if (m_channelIdForRecord.isEmpty()) return;
if (!canAttachFile()) return;
const quint64 context = m_attachmentContext;
QString srcPath = QFileDialog::getOpenFileName(this, QCoreApplication::translate("ChatGui", "Attach file"));
if (srcPath.isEmpty()) return;
prepareAttachment(srcPath, context);
}
/* Выбор через скрепку и drop одинаково проверяют файл и заменяют прежний preview только после успеха. */
bool InputBar::prepareAttachment(const QString& srcPath, quint64 context) {
if (context != m_attachmentContext || !canAttachFile()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Attachment cancelled: the chat changed or attachments are unavailable."), srcPath);
return false;
}
QFileInfo fi(srcPath);
if (m_dmMode && !QStringList{"mp4", "mov", "mkv", "avi", "webm", "m4v"}.contains(fi.suffix().toLower())) {
m_pendingAttachSrc = srcPath;
m_pendingAttachDest = srcPath;
m_pendingAttachName = fi.fileName();
m_pendingAttachSize = fi.size();
m_pendingAttachAsVideo = m_pendingAttachAsImage = false;
m_attachPreviewLabel->setText(QString::fromUtf8("📎 %1 (%2)").arg(fi.fileName(), formatFileSize(fi.size())));
m_attachPreview->show();
GUI_INFO("InputBar: PM attachment selected name=%s bytes=%lld", qPrintable(fi.fileName()), (long long)fi.size());
return;
if (!fi.isFile() || !fi.isReadable()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Select an existing, readable file."), srcPath); return false;
}
if (fi.size() == 0 && (!m_dmMode || isVideoFile(fi.suffix()))) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Cannot attach an empty file."), srcPath); return false;
}
if (fi.size() == 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "InputBar::onAttachClicked: empty file"); return; }
QString mediaPath = m_mediaDirBase + "/" + m_channelIdForRecord;
VoiceEncoder::ensureMediaDir(mediaPath);
QDir().mkpath(mediaPath);
QDateTime now = QDateTime::currentDateTime();
QString ts = now.toString("yyyyMMdd-HHmmss");
int rnd = rand() & 0xFFFF;
bool asImage = !m_dmMode && isImageFile(fi.suffix());
bool asVideo = false;
if (!asImage && QStringList{"mp4", "mov", "mkv", "avi", "webm", "m4v"}.contains(fi.suffix().toLower())) {
if (!asImage && isVideoFile(fi.suffix())) {
QMessageBox box(this);
box.setWindowTitle(QCoreApplication::translate("ChatGui", "Send video"));
box.setText(QCoreApplication::translate("ChatGui", "The file contains video. How would you like to send it?"));
@ -368,14 +449,27 @@ void InputBar::onAttachClicked() {
box.exec();
asVideo = (box.clickedButton() == videoBtn);
if (!box.clickedButton() || (!asVideo && box.clickedButton() != fileBtn)) {
return; /* закрыт без выбора */
GUI_DEBUG("attachment selection cancelled: video mode source=%s", qPrintable(srcPath));
return false;
}
GUI_INFO("InputBar::onAttachClicked: video detected, mode=%s",
GUI_INFO("InputBar: video detected, mode=%s",
asVideo ? "video" : "file");
}
if (context != m_attachmentContext || !canAttachFile()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Attachment cancelled: the chat changed or attachments are unavailable."), srcPath);
return false;
}
const QString mediaPath = m_mediaDirBase + "/" + m_channelIdForRecord;
if ((!m_dmMode || asVideo) && !QDir().mkpath(mediaPath)) {
GUI_ERROR("attachment media directory unavailable: target=%s path=%s", qPrintable(m_channelIdForRecord), qPrintable(mediaPath));
rejectAttachment(QCoreApplication::translate("ChatGui", "Cannot create the attachment directory."), srcPath); return false;
}
const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss");
const int rnd = rand() & 0xFFFF;
QString destPath;
if (asVideo) {
if (m_dmMode && !asVideo) destPath = srcPath;
else if (asVideo) {
destPath = QString("%1/%2_%3_%4.mp4")
.arg(mediaPath, ts).arg(fi.completeBaseName()).arg(rnd, 4, 16, QChar('0'));
} else {
@ -396,8 +490,10 @@ void InputBar::onAttachClicked() {
m_attachPreviewLabel->setText(labelText);
m_attachPreview->show();
GUI_INFO("InputBar::onAttachClicked: pending %s size=%lld video=%d image=%d",
qPrintable(srcPath), (long long)fi.size(), asVideo ? 1 : 0, asImage ? 1 : 0);
GUI_INFO("attachment preview ready: target=%s source=%s bytes=%lld video=%d image=%d",
qPrintable(m_channelIdForRecord), qPrintable(srcPath), (long long)fi.size(), asVideo ? 1 : 0, asImage ? 1 : 0);
m_input->setFocus();
return true;
}
void InputBar::clearPendingAttachment() {

10
tools/chatgui/src/inputbar.h

@ -6,6 +6,7 @@
#include <QWidget>
#include <QHash>
#include <QSet>
#include <QPointer>
class QTextEdit;
class QPushButton;
@ -23,6 +24,8 @@ public:
QWidget *parent = nullptr);
void setRecordingContext(const QString& mediaDirBase, const QString& channelId);
/* Один обработчик URL-drop для composer и viewport истории; отправка остаётся по кнопке/Enter. */
void installFileDropTarget(QWidget* target);
/* Общий composer для канала и PM. */
void setDmMode(bool enabled);
@ -36,6 +39,7 @@ signals:
const QString& displayName);
void sendImageAttachment(const QString& target, const QString& mediaSrc, const QString& mediaDest,
const QString& displayName);
void attachmentRejected(const QString& reason);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
@ -83,4 +87,10 @@ private:
QHash<QString, QSet<QByteArray>> m_preparations;
QSet<QString> m_preparationErrors;
void showPreparation();
bool canAttachFile() const;
bool prepareAttachment(const QString& source, quint64 context);
void rejectAttachment(const QString& reason, const QString& source = QString());
QSet<QObject*> m_dropTargets;
QPointer<QLabel> m_dropHint;
quint64 m_attachmentContext = 0;
};

1
tools/chatgui/src/messagelist.cpp

@ -489,6 +489,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
m_inputBar = new InputBar(m_emojiPanel, recorder, this);
layout->addWidget(m_inputBar);
m_inputBar->installFileDropTarget(m_view->viewport());
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) {
if (m_currentChannelId.isEmpty()) {

Loading…
Cancel
Save