diff --git a/tools/chatgui/src/avatars/avatarcrop.cpp b/tools/chatgui/src/avatars/avatarcrop.cpp new file mode 100644 index 00000000..d96e9040 --- /dev/null +++ b/tools/chatgui/src/avatars/avatarcrop.cpp @@ -0,0 +1,90 @@ +#include "avatarcrop.h" +#include "avatarcache.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include "debug_config.h" +} + +namespace { +class CropView : public QWidget { +public: + QImage image; + explicit CropView(QWidget* parent) : QWidget(parent) { setMinimumSize(320, 320); setCursor(Qt::OpenHandCursor); } + QRectF frame() const { const double side = qMin(width(), height()) * 0.8; return {(width()-side)/2, (height()-side)/2, side, side}; } + double scale() const { return qMax(frame().width()/image.width(), frame().height()/image.height()) * zoom; } + void clamp() { + if (image.isNull()) return; + const double s = scale(), mx = (image.width()*s-frame().width())/2, my = (image.height()*s-frame().height())/2; + offset = {qBound(-mx, offset.x(), mx), qBound(-my, offset.y(), my)}; + } + QRectF selection() const { + const double s = scale(); + const QPointF top = rect().center() + offset - QPointF(image.width()*s/2, image.height()*s/2); + const QRectF f = frame(); + return {(f.x()-top.x())/s/image.width(), (f.y()-top.y())/s/image.height(), f.width()/s/image.width(), f.height()/s/image.height()}; + } +protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); p.fillRect(rect(), QColor(20,20,22)); if (image.isNull()) return; + clamp(); const double s = scale(); const QSizeF size(image.width()*s, image.height()*s); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.drawImage(QRectF(QPointF(width()/2.0,height()/2.0)+offset-QPointF(size.width()/2,size.height()/2), size), image); + QPainterPath mask; mask.addRect(rect()); mask.addRect(frame()); p.fillPath(mask, QColor(0,0,0,150)); + p.setPen(QPen(Qt::white, 2)); p.drawRect(frame()); + } + void wheelEvent(QWheelEvent* e) override { + if (image.isNull()) return; + const double previous = zoom; zoom = qBound(1.0, zoom * (e->angleDelta().y()>0 ? 1.15 : 1/1.15), 16.0); + offset *= zoom/previous; clamp(); update(); e->accept(); + } + void mousePressEvent(QMouseEvent* e) override { if (e->button()==Qt::LeftButton) { last=e->pos(); dragging=true; } } + void mouseMoveEvent(QMouseEvent* e) override { if (dragging) { offset+=e->pos()-last; last=e->pos(); clamp(); update(); } } + void mouseReleaseEvent(QMouseEvent*) override { dragging=false; } +private: + double zoom = 1; QPointF offset; QPoint last; bool dragging = false; +}; +} + +bool selectAvatarCrop(const QString& source, QRectF& crop, QWidget* parent) { + QDialog dialog(parent); dialog.setWindowTitle(QCoreApplication::translate("ChatGui", "Crop avatar")); dialog.resize(620,650); + auto* layout = new QVBoxLayout(&dialog); auto* view = new CropView(&dialog); layout->addWidget(view, 1); + auto* status = new QLabel(QCoreApplication::translate("ChatGui", "Drag photo and use the wheel to select a square"), &dialog); + layout->addWidget(status); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + buttons->button(QDialogButtonBox::Ok)->setText(QCoreApplication::translate("ChatGui", "Upload avatar")); + buttons->button(QDialogButtonBox::Ok)->setEnabled(false); layout->addWidget(buttons); + QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + QPointer guard(view); QPointer owner(&dialog); + QThreadPool::globalInstance()->start(QRunnable::create([source, guard, owner, status, buttons] { + QImageReader reader(source); reader.setAutoTransform(true); const QSize size = reader.size(); QImage image; + if (QFileInfo(source).size() <= AvatarCache::OriginalMaxBytes && size.isValid() && + qint64(size.width())*size.height() <= 16*1024*1024 && + (reader.format()=="jpeg" || reader.format()=="png" || reader.format()=="webp")) { + reader.setScaledSize(size.scaled(1600,1600,Qt::KeepAspectRatio)); image = reader.read(); + } + if (!owner) return; + QMetaObject::invokeMethod(owner, [guard, image, status, buttons] { + if (!guard) return; + if (image.isNull()) { + DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: editor cannot decode selected source"); + status->setText(QCoreApplication::translate("ChatGui", "Cannot load avatar image")); return; + } + guard->image=image; guard->update(); buttons->button(QDialogButtonBox::Ok)->setEnabled(true); + }, Qt::QueuedConnection); + })); + if (dialog.exec()!=QDialog::Accepted) { DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "avatar: crop cancelled"); return false; } + crop=view->selection(); DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "avatar: crop selected x=%.5f y=%.5f w=%.5f h=%.5f", + crop.x(), crop.y(), crop.width(), crop.height()); return true; +} diff --git a/tools/chatgui/src/avatars/avatarcrop.h b/tools/chatgui/src/avatars/avatarcrop.h new file mode 100644 index 00000000..41d3dd94 --- /dev/null +++ b/tools/chatgui/src/avatars/avatarcrop.h @@ -0,0 +1,6 @@ +/* Модальный выбор квадрата: рамка неподвижна, фото перемещается и масштабируется. */ +#pragma once +#include +#include +class QWidget; +bool selectAvatarCrop(const QString& source, QRectF& crop, QWidget* parent); diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index b0be30d4..48e9dca8 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/tools/chatgui/src/settingsdialog.cpp @@ -10,6 +10,7 @@ #include "storagesettingspage.h" #include "radiosettingspage.h" #include "avatars/avatarmanager.h" +#include "avatars/avatarcrop.h" #include "ptt_hotkey_manager.h" #include "connmonitorwindow.h" #include "../transport/gui_bridge.h" @@ -118,7 +119,9 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod connect(upload, &QPushButton::clicked, controls, [avatars, server, controls] { const QString path = QFileDialog::getOpenFileName(controls, QCoreApplication::translate("ChatGui", "Choose avatar"), {}, QCoreApplication::translate("ChatGui", "Images (*.png *.jpg *.jpeg *.webp)")); - if (!path.isEmpty()) avatars->uploadDefault(path, server->currentData().toULongLong()); + QRectF crop; + if (!path.isEmpty() && selectAvatarCrop(path, crop, controls)) + avatars->uploadDefault(path, server->currentData().toULongLong(), crop); }); connect(clear, &QPushButton::clicked, avatars, &AvatarManager::clearDefault); update(); }