/* 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/scene/scene.h" #include "editor/scene/scene_item_canvas.h" #include "editor/scene/scene_item_line.h" #include "editor/scene/scene_item_sticker.h" #include "editor/scene/scene_item_text.h" #include "editor/scene/scene_emoji_document.h" #include "ui/image/image_prepare.h" #include "ui/rp_widget.h" #include "styles/style_editor.h" #include #include #include #include #include #include namespace Editor { namespace { using ItemPtr = std::shared_ptr; class ItemEraser final : public NumberedItem { public: struct Target { std::shared_ptr item; QPixmap before; }; ItemEraser( QPixmap mask, QPointF maskPos, std::vector targets) : _mask(std::move(mask)) , _maskPos(maskPos) , _targets(std::move(targets)) { } void apply() { for (const auto &target : _targets) { target.item->applyEraser(_mask, _maskPos); } } void revert() { for (const auto &target : _targets) { target.item->setPixmap(target.before); } } QRectF boundingRect() const override { return QRectF(); } void paint( QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override { } bool hasState(SaveState state) const override { const auto &saved = (state == SaveState::Keep) ? _keeped : _saved; return saved.saved; } void save(SaveState state) override { auto &saved = (state == SaveState::Keep) ? _keeped : _saved; saved = { .saved = true, .status = status(), }; } void restore(SaveState state) override { if (!hasState(state)) { return; } const auto &saved = (state == SaveState::Keep) ? _keeped : _saved; setStatus(saved.status); if (saved.status == Status::Normal) { apply(); } else if (saved.status == Status::Undid) { revert(); } } private: QPixmap _mask; QPointF _maskPos; std::vector _targets; struct { bool saved = false; NumberedItem::Status status; } _saved, _keeped; }; bool SkipMouseEvent(not_null event) { return event->isAccepted() || (event->button() == Qt::RightButton); } constexpr auto kPaddingFactor = 0.4; constexpr auto kMaxWidthFactor = 0.8; constexpr auto kMinWidthFactor = 0.16; constexpr auto kIdealWidthExtra = 2; constexpr auto kDefaultFontSizeDivisor = 15.; constexpr auto kScaleThreshold = 0.01; class TextEditProxy final : public QGraphicsTextItem { public: using QGraphicsTextItem::QGraphicsTextItem; Fn onFinish; Fn onCancel; protected: void keyPressEvent(QKeyEvent *event) override { if (event->key() == Qt::Key_Escape) { fire(onCancel); return; } QGraphicsTextItem::keyPressEvent(event); } void focusOutEvent(QFocusEvent *event) override { QGraphicsTextItem::focusOutEvent(event); fire(onFinish); } void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override { event->accept(); } private: void fire(Fn &callback) { if (!callback) { return; } const auto cb = std::exchange(callback, nullptr); onFinish = nullptr; onCancel = nullptr; crl::on_main(cb); } }; } // namespace Scene::Scene(const QRectF &rect) : QGraphicsScene(rect) , _canvas(std::make_shared()) , _lastZ(std::make_shared(9000.)) { QGraphicsScene::addItem(_canvas.get()); _canvas->clearPixmap(); _canvas->grabContentRequests( ) | rpl::on_next([=](ItemCanvas::Content &&content) { if (content.clear) { auto mask = std::move(content.pixmap); if (mask.isNull()) { return; } const auto maskPos = content.position; const auto maskSize = mask.size() / float64(mask.devicePixelRatio()); const auto maskRect = QRectF(maskPos, maskSize); auto targets = std::vector(); const auto hits = QGraphicsScene::items( maskRect, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder); for (auto *raw : hits) { const auto it = _itemsByPointer.find(raw); if (it == end(_itemsByPointer)) { continue; } const auto &item = it->second; if (!item->isNormalStatus()) { continue; } const auto line = std::dynamic_pointer_cast(item); if (!line) { continue; } auto before = line->pixmap(); if (!line->applyEraser(mask, maskPos)) { continue; } targets.push_back({ .item = line, .before = std::move(before), }); } if (!targets.empty()) { const auto eraser = std::make_shared( std::move(mask), maskPos, std::move(targets)); addItem(eraser); _canvas->setZValue(++_lastLineZ); } return; } if (content.blur) { auto mask = std::move(content.pixmap); if (mask.isNull() || !_blurSource) { return; } const auto maskPos = content.position; const auto maskSize = mask.size() / float64(mask.devicePixelRatio()); const auto sourceRect = QRectF(maskPos, maskSize); const auto expandedRect = sourceRect.toAlignedRect().adjusted( -st::photoEditorBlurRadius, -st::photoEditorBlurRadius, st::photoEditorBlurRadius, st::photoEditorBlurRadius); const auto captureRect = expandedRect.intersected( sceneRect().toAlignedRect()); if (captureRect.isEmpty()) { return; } auto source = _blurSource(captureRect); if (source.isNull()) { return; } const auto sourceDpr = source.devicePixelRatio(); if (source.format() != QImage::Format_ARGB32_Premultiplied) { source = source.convertToFormat( QImage::Format_ARGB32_Premultiplied); source.setDevicePixelRatio(sourceDpr); } const auto canvasVisible = _canvas->isVisible(); _canvas->setVisible(false); { auto p = QPainter(&source); render( &p, QRectF(QPointF(), QSizeF(captureRect.size())), QRectF(captureRect), Qt::IgnoreAspectRatio); } _canvas->setVisible(canvasVisible); auto blurred = Images::BlurLargeImage( std::move(source), st::photoEditorBlurRadius); if (blurred.isNull()) { return; } blurred.setDevicePixelRatio(sourceDpr); auto result = QImage( mask.size(), QImage::Format_ARGB32_Premultiplied); result.setDevicePixelRatio(mask.devicePixelRatio()); result.fill(Qt::transparent); { auto p = QPainter(&result); p.drawImage( QRectF(QPointF(), maskSize), blurred, QRectF( sourceRect.x() - captureRect.x(), sourceRect.y() - captureRect.y(), sourceRect.width(), sourceRect.height())); p.setCompositionMode( QPainter::CompositionMode_DestinationIn); p.drawPixmap(0, 0, mask); } auto blurPixmap = QPixmap::fromImage(std::move(result)); const auto item = std::make_shared( std::move(blurPixmap)); item->setPos(maskPos); addItem(item); _canvas->setZValue(++_lastLineZ); return; } const auto item = std::make_shared( std::move(content.pixmap)); item->setPos(content.position); addItem(item); _canvas->setZValue(++_lastLineZ); }, _lifetime); QObject::connect( this, &QGraphicsScene::selectionChanged, [=] { const auto selected = selectedItems(); auto *textItem = (ItemText*)(nullptr); if (selected.size() == 1 && selected.front()->type() == ItemText::Type) { textItem = static_cast(selected.front()); } const auto changed = (textItem != _selectedTextItem); if (!changed) { return; } _selectedTextItem = textItem; if (textItem) { _textItemSelections.fire_copy(textItem->color()); } else { _textItemDeselections.fire({}); } }); } void Scene::cancelDrawing() { if (_textEdit.proxy) { finishTextEditing(false); } _canvas->cancelDrawing(); } void Scene::addItem(ItemPtr item) { if (!item) { return; } item->setNumber(_itemNumber++); QGraphicsScene::addItem(item.get()); const auto raw = item.get(); _items.push_back(std::move(item)); _itemsByPointer.emplace(raw, _items.back()); _addsItem.fire({}); } void Scene::removeItem(not_null item) { const auto it = ranges::find_if(_items, [&](const ItemPtr &i) { return i.get() == item; }); if (it == end(_items)) { return; } removeItem(*it); } void Scene::removeItem(const ItemPtr &item) { item->setStatus(NumberedItem::Status::Removed); _removesItem.fire({}); } void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (_textEdit.proxy) { const auto clickOnProxy = _textEdit.proxy->contains( _textEdit.proxy->mapFromScene(event->scenePos())); if (!clickOnProxy) { finishTextEditing(true); QGraphicsScene::mousePressEvent(event); return; } } QGraphicsScene::mousePressEvent(event); if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) { return; } _canvas->handleMousePressEvent(event); } void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { QGraphicsScene::mouseReleaseEvent(event); if (SkipMouseEvent(event) || _textEdit.proxy) { return; } _canvas->handleMouseReleaseEvent(event); } void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QGraphicsScene::mouseMoveEvent(event); if (SkipMouseEvent(event) || _textEdit.proxy) { return; } _canvas->handleMouseMoveEvent(event); } void Scene::applyBrush(const QColor &color, float64 size, Brush::Tool tool) { _canvas->applyBrush(color, size, tool); } void Scene::setTextDefaults( const QColor &color, float64 fontSize, int style) { _textColor = color; _textFontSize = fontSize; _textStyle = style; } void Scene::setTextColor(const QColor &color) { _textColor = color; if (_textEdit.proxy) { _textEdit.proxy->setDefaultTextColor(EffectiveTextColor( color, static_cast(_textEditStyle))); } } void Scene::setSelectedTextColor(const QColor &color) { for (auto *item : selectedItems()) { if (item->type() == ItemText::Type) { static_cast(item)->setColor(color); } } } rpl::producer Scene::textColorRequests() const { return _textColorRequests.events(); } rpl::producer Scene::textItemSelections() const { return _textItemSelections.events(); } rpl::producer<> Scene::textItemDeselections() const { return _textItemDeselections.events(); } rpl::producer Scene::textEditStates() const { return _textEditStates.events(); } void Scene::setBlurSource(Fn source) { _blurSource = std::move(source); } rpl::producer<> Scene::addsItem() const { return _addsItem.events(); } rpl::producer<> Scene::removesItem() const { return _removesItem.events(); } std::vector Scene::items( Qt::SortOrder order) const { auto copyItems = _items; ranges::sort(copyItems, [&](ItemPtr a, ItemPtr b) { const auto numA = a->number(); const auto numB = b->number(); return (order == Qt::AscendingOrder) ? (numA < numB) : (numA > numB); }); return copyItems; } std::shared_ptr Scene::lastZ() const { return _lastZ; } void Scene::updateZoom(float64 zoom) { _currentZoom = zoom; _canvas->updateZoom(zoom); for (const auto &item : items()) { if (item->type() >= ItemBase::Type) { static_cast(item.get())->updateZoom(zoom); } } } bool Scene::hasUndo() const { return ranges::any_of(_items, &NumberedItem::isNormalStatus); } bool Scene::hasRedo() const { return ranges::any_of(_items, &NumberedItem::isUndidStatus); } void Scene::performUndo() { const auto filtered = items(Qt::DescendingOrder); const auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus); if (it != filtered.end()) { if (const auto eraser = dynamic_cast(it->get())) { eraser->revert(); } (*it)->setStatus(NumberedItem::Status::Undid); } } void Scene::performRedo() { const auto filtered = items(Qt::AscendingOrder); const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus); if (it != filtered.end()) { if (const auto eraser = dynamic_cast(it->get())) { eraser->apply(); } (*it)->setStatus(NumberedItem::Status::Normal); } } void Scene::removeIf(Fn proj) { auto copy = std::vector(); for (const auto &item : _items) { const auto toRemove = proj(item); if (toRemove) { // Scene loses ownership of an item. // It seems for some reason this line causes a crash. =( // QGraphicsScene::removeItem(item.get()); } else { copy.push_back(item); } } _items = std::move(copy); _itemsByPointer.clear(); for (const auto &item : _items) { _itemsByPointer.emplace(item.get(), item); } } void Scene::clearRedoList() { for (const auto &item : _items) { if (item->isUndidStatus()) { item->setStatus(NumberedItem::Status::Removed); } } } void Scene::save(SaveState state) { if (_textEdit.proxy) { finishTextEditing(true); } removeIf([](const ItemPtr &item) { return item->isRemovedStatus() && !item->hasState(SaveState::Keep) && !item->hasState(SaveState::Save); }); for (const auto &item : _items) { item->save(state); } clearSelection(); cancelDrawing(); } void Scene::restore(SaveState state) { removeIf([=](const ItemPtr &item) { return !item->hasState(state); }); for (const auto &item : _items) { item->restore(state); } clearSelection(); cancelDrawing(); } void Scene::setTextEditing(bool editing) { if (_textEditing == editing) { return; } _textEditing = editing; _textEditStates.fire_copy(editing); } void Scene::setupTextProxy( QGraphicsTextItem *proxy, const QColor &color, float64 fontSize) { proxy->setTextInteractionFlags(Qt::TextEditorInteraction); proxy->setDefaultTextColor(color); auto *emojiDoc = new EmojiDocument(proxy); emojiDoc->setDocumentMargin(0); proxy->setDocument(emojiDoc); auto font = QFont(); font.setPixelSize(int(fontSize)); font.setWeight(QFont::DemiBold); proxy->setFont(font); { auto option = emojiDoc->defaultTextOption(); option.setAlignment(Qt::AlignCenter); emojiDoc->setDefaultTextOption(option); } } void Scene::createTextAtCenter() { if (_textEdit.proxy) { return; } const auto generation = ++_textEditGeneration; clearSelection(); cancelDrawing(); setTextEditing(true); _textEditStyle = _textStyle; _textEdit.proxy.reset(new TextEditProxy()); const auto proxy = _textEdit.proxy.get(); setupTextProxy( proxy, EffectiveTextColor( _textColor, static_cast(_textEditStyle)), _textFontSize); const auto emojiDoc = proxy->document(); const auto shortSide = std::min( sceneRect().width(), sceneRect().height()); const auto padding = int(_textFontSize * kPaddingFactor); const auto maxTextWidth = std::max( int(shortSide * kMaxWidthFactor) - 2 * padding, 1); const auto minTextWidth = std::clamp( int(shortSide * kMinWidthFactor) - 2 * padding, 1, maxTextWidth); const auto sceneCenter = sceneRect().center(); const auto adjustWidth = [=] { emojiDoc->setTextWidth(maxTextWidth); const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto width = std::clamp( ideal + kIdealWidthExtra, minTextWidth, maxTextWidth); proxy->setTextWidth(width); proxy->setPos(sceneCenter.x() - width / 2., sceneCenter.y()); }; adjustWidth(); QObject::connect(emojiDoc, &QTextDocument::contentsChanged, [=] { ReplaceEmoji(emojiDoc); adjustWidth(); }); QGraphicsScene::addItem(proxy); proxy->setZValue((*_lastZ)++); proxy->setFocus(); if (!views().isEmpty()) { views().first()->setFocus(); } const auto raw = static_cast(proxy); raw->onFinish = crl::guard(this, [=] { if (generation == _textEditGeneration) { finishTextEditing(true); } }); raw->onCancel = crl::guard(this, [=] { if (generation == _textEditGeneration) { finishTextEditing(false); } }); _textEdit.item.reset(); _textColorRequests.fire_copy(_textColor); } void Scene::startTextEditing(ItemText *item) { if (_textEdit.proxy) { finishTextEditing(true); } if (!item) { return; } const auto generation = ++_textEditGeneration; cancelDrawing(); setTextEditing(true); _textEditStyle = int(item->textStyle()); _textEdit.proxy.reset(new TextEditProxy()); const auto proxy = _textEdit.proxy.get(); setupTextProxy( proxy, EffectiveTextColor(item->color(), item->textStyle()), item->fontSize()); proxy->setPlainText(item->text()); ReplaceEmoji(proxy->document()); const auto emojiDoc = proxy->document(); const auto shortSide = std::min( sceneRect().width(), sceneRect().height()); const auto padding = int(item->fontSize() * kPaddingFactor); const auto maxTextWidth = std::max( int(shortSide * kMaxWidthFactor) - 2 * padding, 1); const auto minTextWidth = std::clamp( int(shortSide * kMinWidthFactor) - 2 * padding, 1, maxTextWidth); const auto anchor = item->scenePos(); const auto adjustWidth = [=] { emojiDoc->setTextWidth(maxTextWidth); const auto ideal = int(std::ceil(emojiDoc->idealWidth())); const auto width = std::clamp( ideal + kIdealWidthExtra, minTextWidth, maxTextWidth); proxy->setTextWidth(width); const auto center = proxy->boundingRect().center(); proxy->setTransformOriginPoint(center); proxy->setPos(anchor - center); }; adjustWidth(); QObject::connect(emojiDoc, &QTextDocument::contentsChanged, [=] { ReplaceEmoji(emojiDoc); adjustWidth(); }); const auto scale = item->editScale(); proxy->setRotation(item->rotation()); if (std::abs(scale - 1.) > kScaleThreshold) { proxy->setScale(scale); } QGraphicsScene::addItem(proxy); proxy->setZValue((*_lastZ)++); proxy->setFocus(); auto cursor = proxy->textCursor(); cursor.select(QTextCursor::Document); proxy->setTextCursor(cursor); item->setVisible(false); const auto raw = static_cast(proxy); raw->onFinish = crl::guard(this, [=] { if (generation == _textEditGeneration) { finishTextEditing(true); } }); raw->onCancel = crl::guard(this, [=] { if (generation == _textEditGeneration) { finishTextEditing(false); } }); const auto it = _itemsByPointer.find(item); _textEdit.item = (it != end(_itemsByPointer)) ? it->second : std::weak_ptr(); _textColorRequests.fire_copy(item->color()); } void Scene::finishTextEditing(bool save) { if (!_textEdit.proxy) { return; } const auto text = save ? RecoverTextFromDocument(_textEdit.proxy->document()).trimmed() : QString(); const auto proxyRect = _textEdit.proxy->boundingRect(); const auto proxyCenter = _textEdit.proxy->pos() + QPointF(proxyRect.width() / 2., proxyRect.height() / 2.); const auto lockedItem = _textEdit.item.lock(); auto *existingItem = lockedItem ? static_cast(lockedItem.get()) : (ItemText*)(nullptr); const auto raw = static_cast(_textEdit.proxy.get()); raw->onFinish = nullptr; raw->onCancel = nullptr; QGraphicsScene::removeItem(_textEdit.proxy.get()); _textEdit.proxy = nullptr; _textEdit.item.reset(); setTextEditing(false); const auto defaultStyle = static_cast(_textStyle); if (!text.isEmpty()) { if (existingItem) { existingItem->setText(text); existingItem->setVisible(true); } else { const auto imageSize = sceneRect().size().toSize(); const auto contentSize = ItemText::computeContentSize( text, _textFontSize, imageSize, defaultStyle); const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.; const auto handleInflate = int( std::ceil(st::photoEditorItemHandleSize / zoom)); const auto size = std::max( contentSize.width() + handleInflate, 1); auto data = ItemBase::Data{ .initialZoom = zoom, .zPtr = _lastZ, .size = size, .x = int(proxyCenter.x()), .y = int(proxyCenter.y()), .imageSize = imageSize, }; auto item = std::make_shared( text, _textColor, _textFontSize, defaultStyle, imageSize, std::move(data)); addItem(item); } } else if (existingItem) { if (save) { removeItem(existingItem); } else { existingItem->setVisible(true); } } } Scene::~Scene() { disconnect(this, &QGraphicsScene::selectionChanged, nullptr, nullptr); if (_textEdit.proxy) { setTextEditing(false); const auto raw = static_cast( _textEdit.proxy.get()); raw->onFinish = nullptr; raw->onCancel = nullptr; QGraphicsScene::removeItem(_textEdit.proxy.get()); _textEdit.proxy = nullptr; } QGraphicsScene::removeItem(_canvas.get()); for (const auto &item : items()) { QGraphicsScene::removeItem(item.get()); } } } // namespace Editor