524 linhas
13 KiB
C++
524 linhas
13 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "editor/editor_paint.h"
|
|
|
|
#include "core/mime_type.h"
|
|
#include "editor/controllers/controllers.h"
|
|
#include "editor/scene/scene_item_canvas.h"
|
|
#include "editor/scene/scene_item_image.h"
|
|
#include "editor/scene/scene_item_sticker.h"
|
|
#include "editor/scene/scene_item_text.h"
|
|
#include "editor/scene/scene.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "lottie/lottie_single_player.h"
|
|
#include "storage/storage_media_prepare.h"
|
|
#include "ui/boxes/confirm_box.h"
|
|
#include "ui/chat/attach/attach_prepare.h"
|
|
#include "ui/rect.h"
|
|
#include "ui/ui_utility.h"
|
|
|
|
#include <QGraphicsView>
|
|
#include <QScrollBar>
|
|
#include <QWheelEvent>
|
|
#include <QtCore/QMimeData>
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
constexpr auto kMaxBrush = 25.;
|
|
constexpr auto kMinBrush = 1.;
|
|
constexpr auto kMinCanvasZoom = 1.;
|
|
constexpr auto kMaxCanvasZoom = 8.;
|
|
constexpr auto kCanvasZoomStep = 1.15;
|
|
constexpr auto kZoomEpsilon = 0.0001;
|
|
constexpr auto kMinItemZoom = 0.1;
|
|
constexpr auto kMaxItemZoom = 10.;
|
|
constexpr auto kCanvasZoomStepFine = 1.015;
|
|
|
|
std::shared_ptr<Scene> EnsureScene(
|
|
PhotoModifications &mods,
|
|
const QSize &size) {
|
|
if (!mods.paint) {
|
|
mods.paint = std::make_shared<Scene>(QRectF(QPointF(), size));
|
|
}
|
|
return mods.paint;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
using ItemPtr = std::shared_ptr<QGraphicsItem>;
|
|
|
|
Paint::Paint(
|
|
not_null<Ui::RpWidget*> parent,
|
|
PhotoModifications &modifications,
|
|
const QSize &imageSize,
|
|
std::shared_ptr<Controllers> controllers,
|
|
Fn<QImage(QRect)> blurSource,
|
|
bool fixedCrop)
|
|
: RpWidget(parent)
|
|
, _controllers(controllers)
|
|
, _scene(EnsureScene(modifications, imageSize))
|
|
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
|
|
, _viewport(_view->viewport())
|
|
, _imageSize(imageSize)
|
|
, _fixedCrop(fixedCrop) {
|
|
Expects(modifications.paint != nullptr);
|
|
|
|
_scene->setBlurSource(std::move(blurSource));
|
|
|
|
{
|
|
constexpr auto kDefaultFontSizeDivisor = 15.;
|
|
const auto shortSide = std::min(
|
|
imageSize.width(),
|
|
imageSize.height());
|
|
_scene->setTextDefaults(
|
|
QColor(255, 255, 255),
|
|
shortSide / kDefaultFontSizeDivisor,
|
|
int(TextStyle::Plain));
|
|
}
|
|
|
|
keepResult();
|
|
|
|
_view->show();
|
|
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
_view->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
|
|
_view->setBackgroundBrush(Qt::transparent);
|
|
_view->setAttribute(Qt::WA_TranslucentBackground, true);
|
|
_viewport->setAutoFillBackground(false);
|
|
_viewport->setAttribute(Qt::WA_TranslucentBackground, true);
|
|
_viewport->installEventFilter(this);
|
|
|
|
_scene->textEditStates(
|
|
) | rpl::on_next([=](bool editing) {
|
|
_textEditing = editing;
|
|
}, lifetime());
|
|
|
|
// Undo / Redo.
|
|
controllers->undoController->performRequestChanges(
|
|
) | rpl::on_next([=](const Undo &command) {
|
|
if (_textEditing.current()) {
|
|
return;
|
|
}
|
|
if (command == Undo::Undo) {
|
|
_scene->performUndo();
|
|
} else {
|
|
_scene->performRedo();
|
|
}
|
|
|
|
_hasUndo = _scene->hasUndo();
|
|
_hasRedo = _scene->hasRedo();
|
|
}, lifetime());
|
|
|
|
controllers->undoController->setCanPerformChanges(rpl::merge(
|
|
rpl::combine(
|
|
_hasUndo.value(),
|
|
_textEditing.value()
|
|
) | rpl::map([](bool enable, bool editing) {
|
|
return UndoController::EnableRequest{
|
|
.command = Undo::Undo,
|
|
.enable = enable && !editing,
|
|
};
|
|
}),
|
|
rpl::combine(
|
|
_hasRedo.value(),
|
|
_textEditing.value()
|
|
) | rpl::map([](bool enable, bool editing) {
|
|
return UndoController::EnableRequest{
|
|
.command = Undo::Redo,
|
|
.enable = enable && !editing,
|
|
};
|
|
})));
|
|
|
|
if (controllers->stickersPanelController) {
|
|
using ShowRequest = StickersPanelController::ShowRequest;
|
|
|
|
controllers->stickersPanelController->setShowRequestChanges(
|
|
controllers->stickersPanelController->stickerChosen(
|
|
) | rpl::map_to(ShowRequest::HideAnimated));
|
|
|
|
controllers->stickersPanelController->stickerChosen(
|
|
) | rpl::on_next([=](not_null<DocumentData*> document) {
|
|
const auto item = std::make_shared<ItemSticker>(
|
|
document,
|
|
itemBaseData());
|
|
_scene->addItem(item);
|
|
_scene->clearSelection();
|
|
}, lifetime());
|
|
}
|
|
|
|
rpl::merge(
|
|
controllers->stickersPanelController
|
|
? controllers->stickersPanelController->stickerChosen(
|
|
) | rpl::to_empty
|
|
: rpl::never<>() | rpl::type_erased,
|
|
_scene->addsItem()
|
|
) | rpl::on_next([=] {
|
|
clearRedoList();
|
|
updateUndoState();
|
|
}, lifetime());
|
|
|
|
_scene->removesItem(
|
|
) | rpl::on_next([=] {
|
|
updateUndoState();
|
|
}, lifetime());
|
|
|
|
}
|
|
|
|
bool Paint::zoomSceneItems(float64 wheelDelta, bool fine) {
|
|
if (!wheelDelta) {
|
|
return false;
|
|
}
|
|
const auto step = wheelDelta
|
|
/ float64(QWheelEvent::DefaultDeltasPerStep);
|
|
const auto base = fine ? kCanvasZoomStepFine : kCanvasZoomStep;
|
|
const auto factor = std::pow(base, step);
|
|
const auto center = rect::center(_scene->sceneRect());
|
|
auto applied = false;
|
|
for (const auto &item : _scene->items()) {
|
|
const auto raw = item.get();
|
|
const auto oldScale = raw->scale();
|
|
const auto newScale = std::clamp(
|
|
oldScale * factor,
|
|
kMinItemZoom,
|
|
kMaxItemZoom);
|
|
if (std::abs(newScale - oldScale) < kZoomEpsilon) {
|
|
continue;
|
|
}
|
|
const auto ratio = newScale / oldScale;
|
|
raw->setScale(newScale);
|
|
const auto pos = raw->pos();
|
|
raw->setPos(center + (pos - center) * ratio);
|
|
applied = true;
|
|
}
|
|
return applied;
|
|
}
|
|
|
|
void Paint::panSceneItems(QPointF sceneDelta) {
|
|
if (sceneDelta.isNull()) {
|
|
return;
|
|
}
|
|
for (const auto &item : _scene->items()) {
|
|
item->setPos(item->pos() + sceneDelta);
|
|
}
|
|
}
|
|
|
|
QPointF Paint::mapWidgetDeltaToScene(QPoint delta) const {
|
|
if (!_view) {
|
|
return QPointF(delta);
|
|
}
|
|
return _view->mapToScene(delta) - _view->mapToScene(QPoint());
|
|
}
|
|
|
|
Paint::~Paint() {
|
|
if (_viewport) {
|
|
_viewport->removeEventFilter(this);
|
|
}
|
|
}
|
|
|
|
void Paint::updateViewGeometry() {
|
|
if (_imageGeometry.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto target = (_transform.userZoom - kMinCanvasZoom) > kZoomEpsilon
|
|
? _outerGeometry
|
|
: _imageGeometry;
|
|
if (geometry() != target) {
|
|
setGeometry(target);
|
|
}
|
|
_view->setGeometry(rect());
|
|
}
|
|
|
|
void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
|
|
if (geometry.isEmpty()) {
|
|
return;
|
|
}
|
|
_imageGeometry = geometry;
|
|
_outerGeometry = parentWidget() ? parentWidget()->rect() : geometry;
|
|
|
|
const auto center = (_transform.fitZoom <= 0.)
|
|
|| _view->viewport()->rect().isEmpty()
|
|
? rect::center(_scene->sceneRect())
|
|
: _view->mapToScene(_view->viewport()->rect().center());
|
|
const auto size = geometry.size();
|
|
|
|
const auto rotatedImageSize = QTransform()
|
|
.rotate(angle)
|
|
.mapRect(QRect(QPoint(), _imageSize));
|
|
|
|
const auto ratioW = size.width() / float64(rotatedImageSize.width())
|
|
* (flipped ? -1 : 1);
|
|
const auto ratioH = size.height() / float64(rotatedImageSize.height());
|
|
|
|
_view->setGeometry(rect());
|
|
|
|
_transform = {
|
|
.angle = angle,
|
|
.flipped = flipped,
|
|
.fitZoom = ((std::abs(ratioW) + std::abs(ratioH)) / 2.),
|
|
.ratioW = ratioW,
|
|
.ratioH = ratioH,
|
|
.userZoom = _transform.userZoom,
|
|
};
|
|
updateViewGeometry();
|
|
applyViewTransform();
|
|
_view->centerOn(center);
|
|
if (const auto parent = parentWidget()) {
|
|
parent->update();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Scene> Paint::saveScene() const {
|
|
_scene->save(SaveState::Save);
|
|
return _scene->items().empty()
|
|
? nullptr
|
|
: _scene;
|
|
}
|
|
|
|
void Paint::restoreScene() {
|
|
_scene->restore(SaveState::Save);
|
|
}
|
|
|
|
void Paint::cancel() {
|
|
_scene->restore(SaveState::Keep);
|
|
}
|
|
|
|
void Paint::keepResult() {
|
|
_scene->save(SaveState::Keep);
|
|
}
|
|
|
|
void Paint::clearRedoList() {
|
|
_scene->clearRedoList();
|
|
|
|
_hasRedo = false;
|
|
}
|
|
|
|
void Paint::updateUndoState() {
|
|
_hasUndo = _scene->hasUndo();
|
|
_hasRedo = _scene->hasRedo();
|
|
}
|
|
|
|
void Paint::applyBrush(const Brush &brush) {
|
|
_scene->applyBrush(
|
|
brush.color,
|
|
(kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio),
|
|
brush.tool);
|
|
}
|
|
|
|
void Paint::createTextItem() {
|
|
_scene->createTextAtCenter();
|
|
}
|
|
|
|
void Paint::clearSelection() {
|
|
_scene->clearSelection();
|
|
}
|
|
|
|
void Paint::setTextColor(const QColor &color) {
|
|
_scene->setTextColor(color);
|
|
}
|
|
|
|
void Paint::setSelectedTextColor(const QColor &color) {
|
|
_scene->setSelectedTextColor(color);
|
|
}
|
|
|
|
rpl::producer<QColor> Paint::textColorRequests() const {
|
|
return _scene->textColorRequests();
|
|
}
|
|
|
|
rpl::producer<QColor> Paint::textItemSelections() const {
|
|
return _scene->textItemSelections();
|
|
}
|
|
|
|
rpl::producer<> Paint::textItemDeselections() const {
|
|
return _scene->textItemDeselections();
|
|
}
|
|
|
|
rpl::producer<bool> Paint::textEditStates() const {
|
|
return _scene->textEditStates();
|
|
}
|
|
|
|
void Paint::handleMimeData(const QMimeData *data) {
|
|
const auto add = [&](QImage image) {
|
|
if (image.isNull()) {
|
|
return;
|
|
}
|
|
if (!Ui::ValidateThumbDimensions(image.width(), image.height())) {
|
|
_controllers->show->showBox(
|
|
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
|
|
return;
|
|
}
|
|
|
|
const auto item = std::make_shared<ItemImage>(
|
|
Ui::PixmapFromImage(std::move(image)),
|
|
itemBaseData());
|
|
_scene->addItem(item);
|
|
_scene->clearSelection();
|
|
};
|
|
|
|
using Error = Ui::PreparedList::Error;
|
|
const auto premium = false; // Don't support > 2GB files here.
|
|
const auto list = Core::ReadMimeUrls(data);
|
|
auto result = !list.isEmpty()
|
|
? Storage::PrepareMediaList(
|
|
list.mid(0, 1),
|
|
_imageSize.width() / 2,
|
|
premium)
|
|
: Ui::PreparedList(Error::EmptyFile, QString());
|
|
if (result.error == Error::None) {
|
|
add(base::take(result.files.front().preview));
|
|
} else if (auto read = Core::ReadMimeImage(data)) {
|
|
add(std::move(read.image));
|
|
}
|
|
}
|
|
|
|
void Paint::paintImage(QPainter &p, const QPixmap &image) const {
|
|
if (_view->geometry().isEmpty()) {
|
|
return;
|
|
}
|
|
p.save();
|
|
p.setClipRect(geometry(), Qt::IntersectClip);
|
|
p.translate(pos());
|
|
p.setTransform(_view->viewportTransform(), true);
|
|
p.drawPixmap(Rect(_imageSize), image);
|
|
p.restore();
|
|
}
|
|
|
|
void Paint::resetView() {
|
|
if (_transform.userZoom == kMinCanvasZoom) {
|
|
return;
|
|
}
|
|
_transform.userZoom = kMinCanvasZoom;
|
|
updateViewGeometry();
|
|
applyViewTransform();
|
|
_view->centerOn(rect::center(_scene->sceneRect()));
|
|
if (const auto parent = parentWidget()) {
|
|
parent->update(geometry());
|
|
}
|
|
}
|
|
|
|
ItemBase::Data Paint::itemBaseData() const {
|
|
const auto s = _scene->sceneRect().toRect().size();
|
|
const auto size = std::min(s.width(), s.height()) / 2;
|
|
const auto x = s.width() / 2;
|
|
const auto y = s.height() / 2;
|
|
return ItemBase::Data{
|
|
.initialZoom = _transform.zoom,
|
|
.zPtr = _scene->lastZ(),
|
|
.size = size,
|
|
.x = x,
|
|
.y = y,
|
|
.flipped = _transform.flipped,
|
|
.rotation = -_transform.angle,
|
|
.imageSize = _imageSize,
|
|
};
|
|
}
|
|
|
|
void Paint::applyViewTransform() {
|
|
_view->setTransform(QTransform()
|
|
.scale(
|
|
_transform.ratioW * _transform.userZoom,
|
|
_transform.ratioH * _transform.userZoom)
|
|
.rotate(_transform.angle));
|
|
_transform.zoom = _transform.fitZoom * _transform.userZoom;
|
|
_scene->updateZoom(_transform.zoom);
|
|
}
|
|
|
|
bool Paint::eventFilter(QObject *obj, QEvent *e) {
|
|
if (obj != _viewport) {
|
|
return RpWidget::eventFilter(obj, e);
|
|
}
|
|
const auto view = _view.get();
|
|
if (!view || !_viewport) {
|
|
return true;
|
|
}
|
|
if (e->type() == QEvent::Wheel) {
|
|
const auto wheel = static_cast<QWheelEvent*>(e);
|
|
const auto raw = wheel->angleDelta();
|
|
const auto delta = raw.y() ? raw.y() : raw.x();
|
|
if (!delta) {
|
|
return true;
|
|
}
|
|
|
|
if (_fixedCrop) {
|
|
zoomSceneItems(
|
|
delta,
|
|
wheel->modifiers().testFlag(Qt::ShiftModifier));
|
|
return true;
|
|
}
|
|
const auto step = delta / float64(QWheelEvent::DefaultDeltasPerStep);
|
|
const auto factor = std::pow(kCanvasZoomStep, step);
|
|
const auto newZoom = std::clamp(
|
|
_transform.userZoom * factor,
|
|
kMinCanvasZoom,
|
|
kMaxCanvasZoom);
|
|
if (std::abs(newZoom - _transform.userZoom) < 0.0001) {
|
|
return true;
|
|
}
|
|
|
|
const auto viewportPoint = wheel->position().toPoint();
|
|
const auto globalPoint = _viewport->mapToGlobal(viewportPoint);
|
|
const auto scenePoint = view->mapToScene(viewportPoint);
|
|
_transform.userZoom = newZoom;
|
|
updateViewGeometry();
|
|
applyViewTransform();
|
|
const auto scenePointAfter = view->mapToScene(
|
|
_viewport->mapFromGlobal(globalPoint));
|
|
const auto center = view->mapToScene(rect::center(_viewport->rect()));
|
|
view->centerOn(center - (scenePointAfter - scenePoint));
|
|
if (const auto parent = parentWidget()) {
|
|
parent->update(geometry());
|
|
}
|
|
return true;
|
|
} else if (e->type() == QEvent::MouseButtonPress) {
|
|
const auto mouse = static_cast<QMouseEvent*>(e);
|
|
if (mouse->button() == Qt::MiddleButton) {
|
|
_pan = {
|
|
.active = (_fixedCrop
|
|
|| _transform.userZoom > kMinCanvasZoom),
|
|
.point = mouse->pos(),
|
|
};
|
|
if (_pan.active) {
|
|
_viewport->setCursor(Qt::ClosedHandCursor);
|
|
}
|
|
return true;
|
|
}
|
|
} else if (e->type() == QEvent::MouseMove) {
|
|
const auto mouse = static_cast<QMouseEvent*>(e);
|
|
if (_pan.active) {
|
|
const auto point = mouse->pos();
|
|
const auto delta = point - _pan.point;
|
|
_pan.point = point;
|
|
|
|
if (_fixedCrop) {
|
|
panSceneItems(mapWidgetDeltaToScene(delta));
|
|
} else if (_transform.userZoom > kMinCanvasZoom) {
|
|
view->horizontalScrollBar()->setValue(
|
|
view->horizontalScrollBar()->value() - delta.x());
|
|
view->verticalScrollBar()->setValue(
|
|
view->verticalScrollBar()->value() - delta.y());
|
|
if (const auto parent = parentWidget()) {
|
|
parent->update(geometry());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
} else if (e->type() == QEvent::MouseButtonRelease) {
|
|
const auto mouse = static_cast<QMouseEvent*>(e);
|
|
if (mouse->button() == Qt::MiddleButton) {
|
|
if (_pan.active) {
|
|
_viewport->unsetCursor();
|
|
}
|
|
_pan.active = false;
|
|
return true;
|
|
}
|
|
}
|
|
return RpWidget::eventFilter(obj, e);
|
|
}
|
|
|
|
} // namespace Editor
|