839 linhas
20 KiB
C++
839 linhas
20 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/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 <QGraphicsSceneContextMenuEvent>
|
|
#include <QGraphicsSceneMouseEvent>
|
|
#include <QGraphicsTextItem>
|
|
#include <QGraphicsView>
|
|
#include <QTextCursor>
|
|
#include <QTextDocument>
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
using ItemPtr = std::shared_ptr<NumberedItem>;
|
|
|
|
class ItemEraser final : public NumberedItem {
|
|
public:
|
|
struct Target {
|
|
std::shared_ptr<ItemLine> item;
|
|
QPixmap before;
|
|
};
|
|
|
|
ItemEraser(
|
|
QPixmap mask,
|
|
QPointF maskPos,
|
|
std::vector<Target> 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<Target> _targets;
|
|
|
|
struct {
|
|
bool saved = false;
|
|
NumberedItem::Status status;
|
|
} _saved, _keeped;
|
|
};
|
|
|
|
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> 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<void()> onFinish;
|
|
Fn<void()> 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<void()> &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<ItemCanvas>())
|
|
, _lastZ(std::make_shared<float64>(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<ItemEraser::Target>();
|
|
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<ItemLine>(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<ItemEraser>(
|
|
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<ItemLine>(
|
|
std::move(blurPixmap));
|
|
item->setPos(maskPos);
|
|
addItem(item);
|
|
_canvas->setZValue(++_lastLineZ);
|
|
return;
|
|
}
|
|
const auto item = std::make_shared<ItemLine>(
|
|
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<ItemText*>(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<QGraphicsItem*> 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<TextStyle>(_textEditStyle)));
|
|
}
|
|
}
|
|
|
|
void Scene::setSelectedTextColor(const QColor &color) {
|
|
for (auto *item : selectedItems()) {
|
|
if (item->type() == ItemText::Type) {
|
|
static_cast<ItemText*>(item)->setColor(color);
|
|
}
|
|
}
|
|
}
|
|
|
|
rpl::producer<QColor> Scene::textColorRequests() const {
|
|
return _textColorRequests.events();
|
|
}
|
|
|
|
rpl::producer<QColor> Scene::textItemSelections() const {
|
|
return _textItemSelections.events();
|
|
}
|
|
|
|
rpl::producer<> Scene::textItemDeselections() const {
|
|
return _textItemDeselections.events();
|
|
}
|
|
|
|
rpl::producer<bool> Scene::textEditStates() const {
|
|
return _textEditStates.events();
|
|
}
|
|
|
|
void Scene::setBlurSource(Fn<QImage(QRect)> source) {
|
|
_blurSource = std::move(source);
|
|
}
|
|
|
|
rpl::producer<> Scene::addsItem() const {
|
|
return _addsItem.events();
|
|
}
|
|
|
|
rpl::producer<> Scene::removesItem() const {
|
|
return _removesItem.events();
|
|
}
|
|
|
|
std::vector<ItemPtr> 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<float64> 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<ItemBase*>(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<ItemEraser*>(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<ItemEraser*>(it->get())) {
|
|
eraser->apply();
|
|
}
|
|
(*it)->setStatus(NumberedItem::Status::Normal);
|
|
}
|
|
}
|
|
|
|
void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
|
|
auto copy = std::vector<ItemPtr>();
|
|
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<TextStyle>(_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<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);
|
|
}
|
|
|
|
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<TextEditProxy*>(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<NumberedItem>();
|
|
_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<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();
|
|
setTextEditing(false);
|
|
|
|
const auto defaultStyle = static_cast<TextStyle>(_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<ItemText>(
|
|
text,
|
|
_textColor,
|
|
_textFontSize,
|
|
defaultStyle,
|
|
imageSize,
|
|
std::move(data));
|
|
addItem(item);
|
|
}
|
|
} else if (existingItem) {
|
|
if (save) {
|
|
removeItem(existingItem);
|
|
} else {
|
|
existingItem->setVisible(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Scene::~Scene() {
|
|
if (_textEdit.proxy) {
|
|
setTextEditing(false);
|
|
const auto raw = static_cast<TextEditProxy*>(
|
|
_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
|