/* 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 #include #include #include 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 EnsureScene( PhotoModifications &mods, const QSize &size) { if (!mods.paint) { mods.paint = std::make_shared(QRectF(QPointF(), size)); } return mods.paint; } } // namespace using ItemPtr = std::shared_ptr; Paint::Paint( not_null parent, PhotoModifications &modifications, const QSize &imageSize, std::shared_ptr controllers, Fn blurSource, bool fixedCrop) : RpWidget(parent) , _controllers(controllers) , _scene(EnsureScene(modifications, imageSize)) , _view(base::make_unique_q(_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 document) { const auto item = std::make_shared( 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 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 Paint::textColorRequests() const { return _scene->textColorRequests(); } rpl::producer Paint::textItemSelections() const { return _scene->textItemSelections(); } rpl::producer<> Paint::textItemDeselections() const { return _scene->textItemDeselections(); } rpl::producer 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( 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(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(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(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(e); if (mouse->button() == Qt::MiddleButton) { if (_pan.active) { _viewport->unsetCursor(); } _pan.active = false; return true; } } return RpWidget::eventFilter(obj, e); } } // namespace Editor