chatgui: stage dropped files through the attachment composer
This commit is contained in:
1 parent
7b1143b9d4
commit
0279ed84a9
3 files changed
+134
-27
No files matched your search
+123
-27
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user