3 changed files with 100 additions and 1 deletions
@ -0,0 +1,90 @@
|
||||
#include "avatarcrop.h" |
||||
#include "avatarcache.h" |
||||
#include <QDialog> |
||||
#include <QDialogButtonBox> |
||||
#include <QFileInfo> |
||||
#include <QImageReader> |
||||
#include <QLabel> |
||||
#include <QMouseEvent> |
||||
#include <QPainter> |
||||
#include <QPointer> |
||||
#include <QPushButton> |
||||
#include <QRunnable> |
||||
#include <QThreadPool> |
||||
#include <QVBoxLayout> |
||||
#include <QWheelEvent> |
||||
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<CropView> guard(view); QPointer<QDialog> 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; |
||||
} |
||||
@ -0,0 +1,6 @@
|
||||
/* Модальный выбор квадрата: рамка неподвижна, фото перемещается и масштабируется. */ |
||||
#pragma once |
||||
#include <QRectF> |
||||
#include <QString> |
||||
class QWidget; |
||||
bool selectAvatarCrop(const QString& source, QRectF& crop, QWidget* parent); |
||||
Loading…
Reference in new issue