[img-editor] Moved text edit controller and proxy to separated module.

This commit is contained in:
23rd
2026-08-28 17:53:53 +03:00
parent 6e16662fd4
commit e67e0f8b47
5 changed files with 615 additions and 500 deletions

View File

@@ -12,19 +12,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene_item_shape.h"
#include "editor/scene/scene_item_sticker.h"
#include "editor/scene/scene_item_text.h"
#include "editor/scene/scene_emoji_document.h"
#include "editor/scene/scene_text_editing.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"
#include <QGraphicsSceneContextMenuEvent>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsTextItem>
#include <QGraphicsView>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QtMath>
namespace Editor {
@@ -155,134 +149,17 @@ bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
return event->isAccepted() || (event->button() == Qt::RightButton);
}
constexpr auto kMinWidthFactor = 0.16;
constexpr auto kIdealWidthExtra = 2;
constexpr auto kScaleThreshold = 0.01;
constexpr auto kShapeDragThreshold = 4.;
constexpr auto kShapeSnapAngle = 45.;
constexpr auto kDraftShapeOpacity = 0.5;
class TextEditProxy final : public QGraphicsTextItem {
public:
using QGraphicsTextItem::QGraphicsTextItem;
Fn<void()> onFinish;
Fn<void()> onCancel;
void setStyleInfo(
TextStyle style,
const QColor &color,
float64 fontSize) {
prepareGeometryChange();
_style = style;
_color = color;
_fontSize = fontSize;
update();
}
void setStyleColor(const QColor &color) {
_color = color;
update();
}
QRectF boundingRect() const override {
const auto pad = float64(TextBackgroundPadding(_fontSize, _style));
return QGraphicsTextItem::boundingRect().adjusted(
-pad,
-pad,
pad,
pad);
}
void paint(
QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override {
paintBackground(painter);
QGraphicsTextItem::paint(painter, option, widget);
}
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 paintBackground(QPainter *painter) {
const auto bg = TextBackgroundColor(_color, _style);
if (bg.alpha() <= 0) {
return;
}
auto lines = std::vector<TextBackgroundLine>();
const auto doc = document();
for (auto block = doc->begin()
; block.isValid()
; block = block.next()) {
const auto layout = block.layout();
if (!layout) {
continue;
}
const auto origin = layout->position();
for (auto i = 0; i < layout->lineCount(); ++i) {
const auto line = layout->lineAt(i);
const auto left = origin.x() + line.x();
const auto top = origin.y() + line.y();
lines.push_back({
.left = left,
.top = top,
.right = left + float64(line.naturalTextWidth()),
.bottom = top + float64(line.height()),
});
}
}
if (lines.empty()) {
return;
}
const auto path = BuildTextBackgroundPath(
std::move(lines),
_fontSize);
painter->save();
PainterHighQualityEnabler hq(*painter);
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawPath(path);
painter->restore();
}
void fire(Fn<void()> &callback) {
if (!callback) {
return;
}
const auto cb = std::exchange(callback, nullptr);
onFinish = nullptr;
onCancel = nullptr;
crl::on_main(cb);
}
TextStyle _style = TextStyle::Plain;
QColor _color;
float64 _fontSize = 0.;
};
} // namespace
Scene::Scene(const QRectF &rect)
: QGraphicsScene(rect)
, _canvas(std::make_shared<ItemCanvas>())
, _lastZ(std::make_shared<float64>(9000.)) {
, _lastZ(std::make_shared<float64>(9000.))
, _textEdit(std::make_unique<TextEditController>(this)) {
QGraphicsScene::addItem(_canvas.get());
_canvas->clearPixmap();
@@ -449,16 +326,12 @@ Scene::Scene(const QRectF &rect)
}
void Scene::cancelDrawing() {
if (_textEdit.proxy) {
finishTextEditing(false);
}
_textEdit->finishEditing(false);
_canvas->cancelDrawing();
}
void Scene::cancelTextEditing() {
if (_textEdit.proxy) {
finishTextEditing(false, false);
}
_textEdit->finishEditing(false, false);
}
void Scene::addItem(ItemPtr item) {
@@ -506,15 +379,12 @@ void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
return;
}
}
if (_textEdit.proxy) {
const auto clickOnProxy = _textEdit.proxy->contains(
_textEdit.proxy->mapFromScene(event->scenePos()));
if (!clickOnProxy) {
finishTextEditing(true);
QGraphicsScene::mousePressEvent(event);
capturePlacements();
return;
}
if (_textEdit->editing()
&& !_textEdit->proxyContains(event->scenePos())) {
_textEdit->finishEditing(true);
QGraphicsScene::mousePressEvent(event);
capturePlacements();
return;
}
QGraphicsScene::mousePressEvent(event);
@@ -533,7 +403,7 @@ void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
}
QGraphicsScene::mouseReleaseEvent(event);
commitPlacements();
if (SkipMouseEvent(event) || _textEdit.proxy) {
if (SkipMouseEvent(event) || _textEdit->editing()) {
return;
}
_canvas->handleMouseReleaseEvent(event);
@@ -546,7 +416,7 @@ void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
return;
}
QGraphicsScene::mouseMoveEvent(event);
if (SkipMouseEvent(event) || _textEdit.proxy) {
if (SkipMouseEvent(event) || _textEdit->editing()) {
return;
}
_canvas->handleMouseMoveEvent(event);
@@ -563,9 +433,7 @@ void Scene::setPendingShape(std::optional<PendingShape> pending) {
_shapeTool.pending = std::move(pending);
const auto now = _shapeTool.pending.has_value();
if (now) {
if (_textEdit.proxy) {
finishTextEditing(true);
}
_textEdit->finishEditing(true);
clearSelection();
clearFocus();
}
@@ -726,22 +594,11 @@ void Scene::setTextDefaults(
const QColor &color,
float64 fontSize,
TextStyle style) {
_textColor = color;
_textFontSize = fontSize;
_textStyle = style;
_textEdit->setDefaults(color, fontSize, style);
}
void Scene::setTextColor(const QColor &color) {
if (_textEdit.proxy) {
_textEdit.color = color;
_textEdit.proxy->setDefaultTextColor(EffectiveTextColor(
color,
_textEditStyle));
static_cast<TextEditProxy*>(
_textEdit.proxy.get())->setStyleColor(color);
} else {
_textColor = color;
}
_textEdit->setColor(color);
}
void Scene::setSelectedTextColor(const QColor &color) {
@@ -765,7 +622,7 @@ void Scene::setSelectedShapeBrush(
}
rpl::producer<QColor> Scene::textColorRequests() const {
return _textColorRequests.events();
return _textEdit->colorRequests();
}
rpl::producer<QColor> Scene::textItemSelections() const {
@@ -777,7 +634,7 @@ rpl::producer<> Scene::textItemDeselections() const {
}
rpl::producer<bool> Scene::textEditStates() const {
return _textEditStates.events();
return _textEdit->editStates();
}
rpl::producer<QColor> Scene::shapeItemSelections() const {
@@ -838,6 +695,15 @@ std::shared_ptr<float64> Scene::lastZ() const {
return _lastZ;
}
Scene::ItemPtr Scene::itemShared(QGraphicsItem *item) const {
const auto it = _itemsByPointer.find(item);
return (it != end(_itemsByPointer)) ? it->second : nullptr;
}
float64 Scene::currentZoom() const {
return _currentZoom;
}
void Scene::updateZoom(float64 zoom) {
_currentZoom = zoom;
_canvas->updateZoom(zoom);
@@ -946,9 +812,7 @@ void Scene::clearRedoList() {
}
void Scene::save(SaveState state) {
if (_textEdit.proxy) {
finishTextEditing(true);
}
_textEdit->finishEditing(true);
removeIf([](const ItemPtr &item) {
return item->isRemovedStatus()
@@ -975,321 +839,12 @@ void Scene::restore(SaveState state) {
cancelDrawing();
}
void Scene::setTextEditing(bool editing, bool notify) {
if (_textEditing == editing) {
return;
}
_textEditing = editing;
if (notify) {
_textEditStates.fire_copy(editing);
}
}
void Scene::setupTextProxy(
QGraphicsTextItem *proxy,
const QColor &color,
const TextLayoutSpec &spec) {
proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
proxy->setDefaultTextColor(color);
auto *emojiDoc = new EmojiDocument(proxy);
emojiDoc->setDocumentMargin(0);
proxy->setDocument(emojiDoc);
proxy->setFont(spec.font);
{
auto option = emojiDoc->defaultTextOption();
option.setAlignment(Qt::AlignCenter);
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
emojiDoc->setDefaultTextOption(option);
}
}
void Scene::createTextAtCenter(int rotation, bool flipped) {
if (_textEdit.proxy) {
finishTextEditing(true);
}
const auto generation = ++_textEditGeneration;
clearSelection();
cancelDrawing();
setTextEditing(true);
_textEditStyle = _textStyle;
_textEdit.flipped = flipped;
const auto spec = ComputeTextLayoutSpec(
_textFontSize,
sceneRect().size().toSize(),
_textEditStyle);
_textEdit.proxy.reset(new TextEditProxy());
const auto proxy = _textEdit.proxy.get();
setupTextProxy(
proxy,
EffectiveTextColor(_textColor, _textEditStyle),
spec);
static_cast<TextEditProxy*>(proxy)->setStyleInfo(
_textEditStyle,
_textColor,
_textFontSize);
const auto emojiDoc = proxy->document();
const auto shortSide = std::min(
sceneRect().width(),
sceneRect().height());
const auto maxTextWidth = spec.maxTextWidth;
const auto minTextWidth = std::clamp(
int(shortSide * kMinWidthFactor) - 2 * spec.padding,
1,
maxTextWidth);
const auto sceneCenter = sceneRect().center();
const auto adjustWidth = [=] {
if (int(emojiDoc->textWidth()) != maxTextWidth) {
emojiDoc->setTextWidth(maxTextWidth);
}
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp(
ideal + kIdealWidthExtra,
minTextWidth,
maxTextWidth);
if (int(proxy->textWidth()) != width) {
proxy->setTextWidth(width);
}
if (flipped) {
proxy->setTransform(
QTransform().translate(width, 0).scale(-1, 1));
}
const auto anchor = QPointF(width / 2., 0.);
proxy->setTransformOriginPoint(anchor);
proxy->setPos(sceneCenter - anchor);
};
adjustWidth();
proxy->setRotation(flipped ? -rotation : rotation);
QObject::connect(
emojiDoc,
&QTextDocument::contentsChange,
[=](int position, int removed, int added) {
SanitizeRange(emojiDoc, position, position + added);
ReplaceEmojiInRange(emojiDoc, position, position + added);
adjustWidth();
});
QGraphicsScene::addItem(proxy);
proxy->setZValue((*_lastZ)++);
proxy->setFocus();
if (!views().isEmpty()) {
views().first()->setFocus();
}
const auto raw = static_cast<TextEditProxy*>(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);
_textEdit->createAtCenter(rotation, flipped);
}
void Scene::startTextEditing(ItemText *item) {
if (_textEdit.proxy) {
finishTextEditing(true);
}
if (!item) {
return;
}
const auto generation = ++_textEditGeneration;
cancelDrawing();
setTextEditing(true);
_textEditStyle = item->textStyle();
_textEdit.flipped = item->flipped();
const auto spec = ComputeTextLayoutSpec(
item->fontSize(),
sceneRect().size().toSize(),
item->textStyle());
_textEdit.proxy.reset(new TextEditProxy());
const auto proxy = _textEdit.proxy.get();
setupTextProxy(
proxy,
EffectiveTextColor(item->color(), item->textStyle()),
spec);
static_cast<TextEditProxy*>(proxy)->setStyleInfo(
item->textStyle(),
item->color(),
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 maxTextWidth = spec.maxTextWidth;
const auto minTextWidth = std::clamp(
int(shortSide * kMinWidthFactor) - 2 * spec.padding,
1,
maxTextWidth);
const auto anchor = item->scenePos();
const auto flipped = item->flipped();
const auto adjustWidth = [=] {
if (int(emojiDoc->textWidth()) != maxTextWidth) {
emojiDoc->setTextWidth(maxTextWidth);
}
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp(
ideal + kIdealWidthExtra,
minTextWidth,
maxTextWidth);
if (int(proxy->textWidth()) != width) {
proxy->setTextWidth(width);
}
if (flipped) {
proxy->setTransform(
QTransform().translate(width, 0).scale(-1, 1));
}
const auto center = proxy->boundingRect().center();
proxy->setTransformOriginPoint(center);
proxy->setPos(anchor - center);
};
adjustWidth();
QObject::connect(
emojiDoc,
&QTextDocument::contentsChange,
[=](int position, int removed, int added) {
SanitizeRange(emojiDoc, position, position + added);
ReplaceEmojiInRange(emojiDoc, position, position + added);
adjustWidth();
});
const auto scale = item->scale() * item->editScale();
proxy->setRotation(flipped ? -item->rotation() : 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<TextEditProxy*>(proxy);
raw->onFinish = crl::guard(this, [=] {
if ((generation != _textEditGeneration) || !_textEdit.proxy) {
return;
}
// Focus loss with cleared text cancels instead of removing.
const auto empty = RecoverTextFromDocument(
_textEdit.proxy->document()).trimmed().isEmpty();
finishTextEditing(!empty);
});
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<NumberedItem>();
_textColorRequests.fire_copy(item->color());
}
void Scene::finishTextEditing(bool save, bool notify) {
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->mapToScene(proxyRect.center());
const auto flipped = std::exchange(_textEdit.flipped, false);
const auto proxyRotation = int(flipped
? -_textEdit.proxy->rotation()
: _textEdit.proxy->rotation());
const auto lockedItem = _textEdit.item.lock();
auto *existingItem = lockedItem
? static_cast<ItemText*>(lockedItem.get())
: (ItemText*)(nullptr);
const auto raw = static_cast<TextEditProxy*>(_textEdit.proxy.get());
raw->onFinish = nullptr;
raw->onCancel = nullptr;
QGraphicsScene::removeItem(_textEdit.proxy.get());
_textEdit.proxy = nullptr;
_textEdit.item.reset();
const auto stagedColor = base::take(_textEdit.color);
setTextEditing(false, notify);
const auto defaultStyle = _textStyle;
if (!text.isEmpty()) {
if (existingItem) {
if (stagedColor) {
existingItem->setColor(*stagedColor);
}
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()),
.flipped = flipped,
.rotation = proxyRotation,
.imageSize = imageSize,
};
auto item = std::make_shared<ItemText>(
text,
stagedColor.value_or(_textColor),
_textFontSize,
defaultStyle,
imageSize,
std::move(data));
addItem(item);
}
} else if (existingItem) {
if (save) {
removeItem(existingItem);
} else {
existingItem->setVisible(true);
}
}
_textEdit->startEditing(item);
}
Scene::~Scene() {

View File

@@ -7,14 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <base/unique_qptr.h>
#include <editor/photo_editor_inner_common.h>
#include <editor/scene/scene_item_base.h>
#include <QGraphicsScene>
class QGraphicsSceneMouseEvent;
class QGraphicsTextItem;
namespace Ui {
class RpWidget;
@@ -26,7 +24,7 @@ class ItemCanvas;
class ItemShape;
class ItemText;
class NumberedItem;
struct TextLayoutSpec;
class TextEditController;
class Scene final : public QGraphicsScene {
public:
@@ -67,6 +65,8 @@ public:
[[nodiscard]] rpl::producer<> removesItem() const;
[[nodiscard]] std::shared_ptr<float64> lastZ() const;
[[nodiscard]] ItemPtr itemShared(QGraphicsItem *item) const;
[[nodiscard]] float64 currentZoom() const;
void updateZoom(float64 zoom);
@@ -118,15 +118,10 @@ private:
[[nodiscard]] std::shared_ptr<ItemShape> createShape(
int size,
const QPointF &center) const;
void finishTextEditing(bool save, bool notify = true);
void setTextEditing(bool editing, bool notify = true);
void setupTextProxy(
QGraphicsTextItem *proxy,
const QColor &color,
const TextLayoutSpec &spec);
const std::shared_ptr<ItemCanvas> _canvas;
const std::shared_ptr<float64> _lastZ;
const std::unique_ptr<TextEditController> _textEdit;
Fn<QImage(QRect)> _blurSource;
std::vector<ItemPtr> _items;
@@ -137,18 +132,6 @@ private:
float64 _currentZoom = 1.;
int _itemNumber = 0;
QColor _textColor;
float64 _textFontSize = 0.;
TextStyle _textStyle = TextStyle::Plain;
TextStyle _textEditStyle = TextStyle::Plain;
struct {
std::weak_ptr<NumberedItem> item;
base::unique_qptr<QGraphicsTextItem> proxy;
std::optional<QColor> color;
bool flipped = false;
} _textEdit;
struct {
std::optional<PendingShape> pending;
std::shared_ptr<ItemShape> item;
@@ -159,17 +142,13 @@ private:
} _shapeTool;
rpl::event_stream<> _addsItem, _removesItem;
rpl::event_stream<QColor> _textColorRequests;
rpl::event_stream<QColor> _textItemSelections;
rpl::event_stream<> _textItemDeselections;
rpl::event_stream<bool> _textEditStates;
rpl::event_stream<QColor> _shapeItemSelections;
rpl::event_stream<> _shapeItemDeselections;
rpl::event_stream<bool> _pendingShapeStates;
ItemText *_selectedTextItem = nullptr;
ItemShape *_selectedShapeItem = nullptr;
bool _textEditing = false;
int _textEditGeneration = 0;
rpl::lifetime _lifetime;
};

View File

@@ -0,0 +1,510 @@
/*
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_text_editing.h"
#include "editor/scene/scene.h"
#include "editor/scene/scene_emoji_document.h"
#include "editor/scene/scene_item_text.h"
#include "ui/painter.h"
#include "styles/style_editor.h"
#include <QGraphicsSceneContextMenuEvent>
#include <QGraphicsTextItem>
#include <QGraphicsView>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
namespace Editor {
namespace {
constexpr auto kMinWidthFactor = 0.16;
constexpr auto kIdealWidthExtra = 2;
constexpr auto kScaleThreshold = 0.01;
class TextEditProxy final : public QGraphicsTextItem {
public:
using QGraphicsTextItem::QGraphicsTextItem;
Fn<void()> onFinish;
Fn<void()> onCancel;
void setStyleInfo(
TextStyle style,
const QColor &color,
float64 fontSize) {
prepareGeometryChange();
_style = style;
_color = color;
_fontSize = fontSize;
update();
}
void setStyleColor(const QColor &color) {
_color = color;
update();
}
QRectF boundingRect() const override {
const auto pad = float64(TextBackgroundPadding(_fontSize, _style));
return QGraphicsTextItem::boundingRect().adjusted(
-pad,
-pad,
pad,
pad);
}
void paint(
QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override {
paintBackground(painter);
QGraphicsTextItem::paint(painter, option, widget);
}
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 paintBackground(QPainter *painter) {
const auto bg = TextBackgroundColor(_color, _style);
if (bg.alpha() <= 0) {
return;
}
auto lines = std::vector<TextBackgroundLine>();
const auto doc = document();
for (auto block = doc->begin()
; block.isValid()
; block = block.next()) {
const auto layout = block.layout();
if (!layout) {
continue;
}
const auto origin = layout->position();
for (auto i = 0; i < layout->lineCount(); ++i) {
const auto line = layout->lineAt(i);
const auto left = origin.x() + line.x();
const auto top = origin.y() + line.y();
lines.push_back({
.left = left,
.top = top,
.right = left + float64(line.naturalTextWidth()),
.bottom = top + float64(line.height()),
});
}
}
if (lines.empty()) {
return;
}
const auto path = BuildTextBackgroundPath(
std::move(lines),
_fontSize);
painter->save();
PainterHighQualityEnabler hq(*painter);
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawPath(path);
painter->restore();
}
void fire(Fn<void()> &callback) {
if (!callback) {
return;
}
const auto cb = std::exchange(callback, nullptr);
onFinish = nullptr;
onCancel = nullptr;
crl::on_main(cb);
}
TextStyle _style = TextStyle::Plain;
QColor _color;
float64 _fontSize = 0.;
};
} // namespace
TextEditController::TextEditController(not_null<Scene*> scene)
: _scene(scene) {
}
not_null<QGraphicsScene*> TextEditController::graphicsScene() const {
return _scene.get();
}
void TextEditController::setDefaults(
const QColor &color,
float64 fontSize,
TextStyle style) {
_defaultColor = color;
_defaultFontSize = fontSize;
_defaultStyle = style;
}
void TextEditController::setColor(const QColor &color) {
if (_edit.proxy) {
_edit.color = color;
_edit.proxy->setDefaultTextColor(EffectiveTextColor(
color,
_editStyle));
static_cast<TextEditProxy*>(_edit.proxy.get())->setStyleColor(color);
} else {
_defaultColor = color;
}
}
bool TextEditController::editing() const {
return _edit.proxy != nullptr;
}
bool TextEditController::proxyContains(const QPointF &scenePos) const {
return _edit.proxy
&& _edit.proxy->contains(_edit.proxy->mapFromScene(scenePos));
}
rpl::producer<QColor> TextEditController::colorRequests() const {
return _colorRequests.events();
}
rpl::producer<bool> TextEditController::editStates() const {
return _editStates.events();
}
void TextEditController::setEditingState(bool editing, bool notify) {
if (_editingState == editing) {
return;
}
_editingState = editing;
if (notify) {
_editStates.fire_copy(editing);
}
}
void TextEditController::setupProxy(
QGraphicsTextItem *proxy,
const QColor &color,
const TextLayoutSpec &spec) {
proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
proxy->setDefaultTextColor(color);
auto *emojiDoc = new EmojiDocument(proxy);
emojiDoc->setDocumentMargin(0);
proxy->setDocument(emojiDoc);
proxy->setFont(spec.font);
{
auto option = emojiDoc->defaultTextOption();
option.setAlignment(Qt::AlignCenter);
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
emojiDoc->setDefaultTextOption(option);
}
}
void TextEditController::createAtCenter(int rotation, bool flipped) {
if (_edit.proxy) {
finishEditing(true);
}
const auto generation = ++_generation;
_scene->clearSelection();
_scene->cancelDrawing();
setEditingState(true);
_editStyle = _defaultStyle;
_edit.flipped = flipped;
const auto sceneRect = _scene->sceneRect();
const auto spec = ComputeTextLayoutSpec(
_defaultFontSize,
sceneRect.size().toSize(),
_editStyle);
_edit.proxy.reset(new TextEditProxy());
const auto proxy = _edit.proxy.get();
setupProxy(
proxy,
EffectiveTextColor(_defaultColor, _editStyle),
spec);
static_cast<TextEditProxy*>(proxy)->setStyleInfo(
_editStyle,
_defaultColor,
_defaultFontSize);
const auto emojiDoc = proxy->document();
const auto shortSide = std::min(
sceneRect.width(),
sceneRect.height());
const auto maxTextWidth = spec.maxTextWidth;
const auto minTextWidth = std::clamp(
int(shortSide * kMinWidthFactor) - 2 * spec.padding,
1,
maxTextWidth);
const auto sceneCenter = sceneRect.center();
const auto adjustWidth = [=] {
if (int(emojiDoc->textWidth()) != maxTextWidth) {
emojiDoc->setTextWidth(maxTextWidth);
}
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp(
ideal + kIdealWidthExtra,
minTextWidth,
maxTextWidth);
if (int(proxy->textWidth()) != width) {
proxy->setTextWidth(width);
}
if (flipped) {
proxy->setTransform(
QTransform().translate(width, 0).scale(-1, 1));
}
const auto anchor = QPointF(width / 2., 0.);
proxy->setTransformOriginPoint(anchor);
proxy->setPos(sceneCenter - anchor);
};
adjustWidth();
proxy->setRotation(flipped ? -rotation : rotation);
QObject::connect(
emojiDoc,
&QTextDocument::contentsChange,
[=](int position, int removed, int added) {
SanitizeRange(emojiDoc, position, position + added);
ReplaceEmojiInRange(emojiDoc, position, position + added);
adjustWidth();
});
graphicsScene()->addItem(proxy);
proxy->setZValue((*_scene->lastZ())++);
proxy->setFocus();
if (!_scene->views().isEmpty()) {
_scene->views().first()->setFocus();
}
const auto raw = static_cast<TextEditProxy*>(proxy);
raw->onFinish = crl::guard(_scene, [=] {
if (generation == _generation) {
finishEditing(true);
}
});
raw->onCancel = crl::guard(_scene, [=] {
if (generation == _generation) {
finishEditing(false);
}
});
_edit.item.reset();
_colorRequests.fire_copy(_defaultColor);
}
void TextEditController::startEditing(ItemText *item) {
if (_edit.proxy) {
finishEditing(true);
}
if (!item) {
return;
}
const auto generation = ++_generation;
_scene->cancelDrawing();
setEditingState(true);
_editStyle = item->textStyle();
_edit.flipped = item->flipped();
const auto sceneRect = _scene->sceneRect();
const auto spec = ComputeTextLayoutSpec(
item->fontSize(),
sceneRect.size().toSize(),
item->textStyle());
_edit.proxy.reset(new TextEditProxy());
const auto proxy = _edit.proxy.get();
setupProxy(
proxy,
EffectiveTextColor(item->color(), item->textStyle()),
spec);
static_cast<TextEditProxy*>(proxy)->setStyleInfo(
item->textStyle(),
item->color(),
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 maxTextWidth = spec.maxTextWidth;
const auto minTextWidth = std::clamp(
int(shortSide * kMinWidthFactor) - 2 * spec.padding,
1,
maxTextWidth);
const auto anchor = item->scenePos();
const auto flipped = item->flipped();
const auto adjustWidth = [=] {
if (int(emojiDoc->textWidth()) != maxTextWidth) {
emojiDoc->setTextWidth(maxTextWidth);
}
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
const auto width = std::clamp(
ideal + kIdealWidthExtra,
minTextWidth,
maxTextWidth);
if (int(proxy->textWidth()) != width) {
proxy->setTextWidth(width);
}
if (flipped) {
proxy->setTransform(
QTransform().translate(width, 0).scale(-1, 1));
}
const auto center = proxy->boundingRect().center();
proxy->setTransformOriginPoint(center);
proxy->setPos(anchor - center);
};
adjustWidth();
QObject::connect(
emojiDoc,
&QTextDocument::contentsChange,
[=](int position, int removed, int added) {
SanitizeRange(emojiDoc, position, position + added);
ReplaceEmojiInRange(emojiDoc, position, position + added);
adjustWidth();
});
const auto scale = item->scale() * item->editScale();
proxy->setRotation(flipped ? -item->rotation() : item->rotation());
if (std::abs(scale - 1.) > kScaleThreshold) {
proxy->setScale(scale);
}
graphicsScene()->addItem(proxy);
proxy->setZValue((*_scene->lastZ())++);
proxy->setFocus();
auto cursor = proxy->textCursor();
cursor.select(QTextCursor::Document);
proxy->setTextCursor(cursor);
item->setVisible(false);
const auto raw = static_cast<TextEditProxy*>(proxy);
raw->onFinish = crl::guard(_scene, [=] {
if ((generation != _generation) || !_edit.proxy) {
return;
}
// Focus loss with cleared text cancels instead of removing.
const auto empty = RecoverTextFromDocument(
_edit.proxy->document()).trimmed().isEmpty();
finishEditing(!empty);
});
raw->onCancel = crl::guard(_scene, [=] {
if (generation == _generation) {
finishEditing(false);
}
});
_edit.item = _scene->itemShared(item);
_colorRequests.fire_copy(item->color());
}
void TextEditController::finishEditing(bool save, bool notify) {
if (!_edit.proxy) {
return;
}
const auto text = save
? RecoverTextFromDocument(_edit.proxy->document()).trimmed()
: QString();
const auto proxyRect = _edit.proxy->boundingRect();
const auto proxyCenter = _edit.proxy->mapToScene(proxyRect.center());
const auto flipped = std::exchange(_edit.flipped, false);
const auto proxyRotation = int(flipped
? -_edit.proxy->rotation()
: _edit.proxy->rotation());
const auto lockedItem = _edit.item.lock();
auto *existingItem = lockedItem
? static_cast<ItemText*>(lockedItem.get())
: (ItemText*)(nullptr);
const auto raw = static_cast<TextEditProxy*>(_edit.proxy.get());
raw->onFinish = nullptr;
raw->onCancel = nullptr;
graphicsScene()->removeItem(_edit.proxy.get());
_edit.proxy = nullptr;
_edit.item.reset();
const auto stagedColor = base::take(_edit.color);
setEditingState(false, notify);
const auto defaultStyle = _defaultStyle;
if (!text.isEmpty()) {
if (existingItem) {
if (stagedColor) {
existingItem->setColor(*stagedColor);
}
existingItem->setText(text);
existingItem->setVisible(true);
} else {
const auto imageSize = _scene->sceneRect().size().toSize();
const auto contentSize = ItemText::computeContentSize(
text,
_defaultFontSize,
imageSize,
defaultStyle);
const auto currentZoom = _scene->currentZoom();
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 = _scene->lastZ(),
.size = size,
.x = int(proxyCenter.x()),
.y = int(proxyCenter.y()),
.flipped = flipped,
.rotation = proxyRotation,
.imageSize = imageSize,
};
auto item = std::make_shared<ItemText>(
text,
stagedColor.value_or(_defaultColor),
_defaultFontSize,
defaultStyle,
imageSize,
std::move(data));
_scene->addItem(item);
}
} else if (existingItem) {
if (save) {
_scene->removeItem(existingItem);
} else {
existingItem->setVisible(true);
}
}
}
} // namespace Editor

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/photo_editor_inner_common.h"
class QGraphicsScene;
class QGraphicsTextItem;
namespace Editor {
class ItemText;
class NumberedItem;
class Scene;
struct TextLayoutSpec;
class TextEditController final {
public:
explicit TextEditController(not_null<Scene*> scene);
void setDefaults(const QColor &color, float64 fontSize, TextStyle style);
void setColor(const QColor &color);
void createAtCenter(int rotation, bool flipped);
void startEditing(ItemText *item);
void finishEditing(bool save, bool notify = true);
[[nodiscard]] bool editing() const;
[[nodiscard]] bool proxyContains(const QPointF &scenePos) const;
[[nodiscard]] rpl::producer<QColor> colorRequests() const;
[[nodiscard]] rpl::producer<bool> editStates() const;
private:
void setEditingState(bool editing, bool notify = true);
void setupProxy(
QGraphicsTextItem *proxy,
const QColor &color,
const TextLayoutSpec &spec);
[[nodiscard]] not_null<QGraphicsScene*> graphicsScene() const;
const not_null<Scene*> _scene;
QColor _defaultColor;
float64 _defaultFontSize = 0.;
TextStyle _defaultStyle = TextStyle::Plain;
TextStyle _editStyle = TextStyle::Plain;
struct {
std::weak_ptr<NumberedItem> item;
base::unique_qptr<QGraphicsTextItem> proxy;
std::optional<QColor> color;
bool flipped = false;
} _edit;
rpl::event_stream<QColor> _colorRequests;
rpl::event_stream<bool> _editStates;
bool _editingState = false;
int _generation = 0;
};
} // namespace Editor

View File

@@ -197,6 +197,8 @@ PRIVATE
editor/scene/scene_item_text.h
editor/scene/scene_emoji_document.cpp
editor/scene/scene_emoji_document.h
editor/scene/scene_text_editing.cpp
editor/scene/scene_text_editing.h
ui/boxes/about_cocoon_box.h
ui/boxes/about_cocoon_box.cpp