mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
[img-editor] Added styled text items.
This commit is contained in:
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#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"
|
||||
@@ -65,6 +66,14 @@ Paint::Paint(
|
||||
|
||||
_scene->setBlurSource(std::move(blurSource));
|
||||
|
||||
{
|
||||
const auto shortSide = std::min(imageSize.width(), imageSize.height());
|
||||
_scene->setTextDefaults(
|
||||
QColor(255, 255, 255),
|
||||
shortSide / 15.f,
|
||||
int(TextStyle::Plain));
|
||||
}
|
||||
|
||||
keepResult();
|
||||
|
||||
_view->show();
|
||||
@@ -234,6 +243,10 @@ void Paint::applyBrush(const Brush &brush) {
|
||||
brush.tool);
|
||||
}
|
||||
|
||||
void Paint::createTextItem() {
|
||||
_scene->createTextAtCenter();
|
||||
}
|
||||
|
||||
void Paint::handleMimeData(const QMimeData *data) {
|
||||
const auto add = [&](QImage image) {
|
||||
if (image.isNull()) {
|
||||
|
||||
@@ -41,6 +41,8 @@ public:
|
||||
void keepResult();
|
||||
void updateUndoState();
|
||||
|
||||
void createTextItem();
|
||||
|
||||
void handleMimeData(const QMimeData *data);
|
||||
void paintImage(QPainter &p, const QPixmap &image) const;
|
||||
void resetView();
|
||||
|
||||
@@ -302,6 +302,11 @@ PhotoEditor::PhotoEditor(
|
||||
};
|
||||
}, lifetime());
|
||||
|
||||
_controls->textRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_content->createTextItem();
|
||||
}, lifetime());
|
||||
|
||||
_controls->doneRequests(
|
||||
) | rpl::on_next([=] {
|
||||
const auto mode = _mode.current().mode;
|
||||
|
||||
@@ -162,6 +162,10 @@ void PhotoEditorContent::applyBrush(const Brush &brush) {
|
||||
_paint->applyBrush(brush);
|
||||
}
|
||||
|
||||
void PhotoEditorContent::createTextItem() {
|
||||
_paint->createTextItem();
|
||||
}
|
||||
|
||||
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
void applyModifications(PhotoModifications modifications);
|
||||
void applyMode(const PhotoEditorMode &mode);
|
||||
void applyBrush(const Brush &brush);
|
||||
void createTextItem();
|
||||
void applyAspectRatio(float64 ratio);
|
||||
void save(PhotoModifications &modifications);
|
||||
|
||||
|
||||
@@ -202,6 +202,37 @@ ButtonBar::ButtonBar(
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
class TextToolButton final : public Ui::AbstractButton {
|
||||
public:
|
||||
TextToolButton(not_null<QWidget*> parent)
|
||||
: AbstractButton(parent) {
|
||||
resize(
|
||||
st::photoEditorStickersButton.width,
|
||||
st::photoEditorStickersButton.height);
|
||||
events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> event) {
|
||||
if (event->type() == QEvent::Enter
|
||||
|| event->type() == QEvent::Leave) {
|
||||
update();
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *) override {
|
||||
auto p = QPainter(this);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto font = st::semiboldFont->f;
|
||||
font.setPixelSize(QWidget::rect().height() / 2);
|
||||
p.setFont(font);
|
||||
p.setPen(isOver()
|
||||
? st::photoEditorButtonIconFgOver
|
||||
: st::photoEditorButtonIconFg);
|
||||
p.translate(0, st::lineWidth * 3);
|
||||
p.drawText(QWidget::rect(), style::al_center, u"A"_q);
|
||||
}
|
||||
};
|
||||
|
||||
PhotoEditorControls::PhotoEditorControls(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
std::shared_ptr<Controllers> controllers,
|
||||
@@ -272,6 +303,7 @@ PhotoEditorControls::PhotoEditorControls(
|
||||
_paintBottomButtons,
|
||||
st::photoEditorStickersButton)
|
||||
: nullptr)
|
||||
, _textButton(base::make_unique_q<TextToolButton>(_paintBottomButtons))
|
||||
, _paintDone(base::make_unique_q<EdgeButton>(
|
||||
_paintBottomButtons,
|
||||
tr::lng_box_done(tr::now),
|
||||
@@ -499,6 +531,10 @@ rpl::producer<> PhotoEditorControls::paintModeRequests() const {
|
||||
return _paintModeButton->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::textRequests() const {
|
||||
return _textButton->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> PhotoEditorControls::doneRequests() const {
|
||||
return rpl::merge(
|
||||
_transformDone->clicks() | rpl::to_empty,
|
||||
|
||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "editor/photo_editor_inner_common.h"
|
||||
|
||||
namespace Ui {
|
||||
class AbstractButton;
|
||||
class IconButton;
|
||||
class FlatLabel;
|
||||
class PopupMenu;
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
[[nodiscard]] rpl::producer<int> rotateRequests() const;
|
||||
[[nodiscard]] rpl::producer<> flipRequests() const;
|
||||
[[nodiscard]] rpl::producer<> paintModeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> textRequests() const;
|
||||
[[nodiscard]] rpl::producer<> doneRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||
[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;
|
||||
@@ -82,6 +84,7 @@ private:
|
||||
const base::unique_qptr<Ui::IconButton> _redoButton;
|
||||
const base::unique_qptr<Ui::IconButton> _paintModeButtonActive;
|
||||
const base::unique_qptr<Ui::IconButton> _stickersButton;
|
||||
const base::unique_qptr<Ui::AbstractButton> _textButton;
|
||||
const base::unique_qptr<EdgeButton> _paintDone;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _ratioMenu;
|
||||
|
||||
@@ -10,11 +10,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#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 "ui/image/image_prepare.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
#include <QGraphicsSceneContextMenuEvent>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsTextItem>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
@@ -100,6 +106,43 @@ bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
|
||||
return event->isAccepted() || (event->button() == Qt::RightButton);
|
||||
}
|
||||
|
||||
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;
|
||||
QTimer::singleShot(0, cb);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Scene::Scene(const QRectF &rect)
|
||||
@@ -240,6 +283,9 @@ Scene::Scene(const QRectF &rect)
|
||||
}
|
||||
|
||||
void Scene::cancelDrawing() {
|
||||
if (_textEdit.proxy) {
|
||||
finishTextEditing(false);
|
||||
}
|
||||
_canvas->cancelDrawing();
|
||||
}
|
||||
|
||||
@@ -271,6 +317,16 @@ void Scene::removeItem(const ItemPtr &item) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -280,7 +336,7 @@ void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
||||
|
||||
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseReleaseEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
if (SkipMouseEvent(event) || _textEdit.proxy) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseReleaseEvent(event);
|
||||
@@ -288,7 +344,7 @@ void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
|
||||
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseMoveEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
if (SkipMouseEvent(event) || _textEdit.proxy) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseMoveEvent(event);
|
||||
@@ -296,6 +352,17 @@ void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
|
||||
void Scene::applyBrush(const QColor &color, float size, Brush::Tool tool) {
|
||||
_canvas->applyBrush(color, size, tool);
|
||||
for (auto *item : selectedItems()) {
|
||||
if (item->type() == ItemText::Type) {
|
||||
static_cast<ItemText*>(item)->setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::setTextDefaults(const QColor &color, float fontSize, int style) {
|
||||
_textColor = color;
|
||||
_textFontSize = fontSize;
|
||||
_textStyle = style;
|
||||
}
|
||||
|
||||
void Scene::setBlurSource(Fn<QImage(QRect)> source) {
|
||||
@@ -328,6 +395,7 @@ std::shared_ptr<float64> Scene::lastZ() const {
|
||||
}
|
||||
|
||||
void Scene::updateZoom(float64 zoom) {
|
||||
_currentZoom = zoom;
|
||||
_canvas->updateZoom(zoom);
|
||||
for (const auto &item : items()) {
|
||||
if (item->type() >= ItemBase::Type) {
|
||||
@@ -396,6 +464,10 @@ void Scene::clearRedoList() {
|
||||
}
|
||||
|
||||
void Scene::save(SaveState state) {
|
||||
if (_textEdit.proxy) {
|
||||
finishTextEditing(true);
|
||||
}
|
||||
|
||||
removeIf([](const ItemPtr &item) {
|
||||
return item->isRemovedStatus()
|
||||
&& !item->hasState(SaveState::Keep)
|
||||
@@ -421,7 +493,183 @@ void Scene::restore(SaveState state) {
|
||||
cancelDrawing();
|
||||
}
|
||||
|
||||
void Scene::createTextAtCenter() {
|
||||
if (_textEdit.proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearSelection();
|
||||
cancelDrawing();
|
||||
|
||||
auto *proxy = new TextEditProxy();
|
||||
proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
proxy->setDefaultTextColor(_textColor);
|
||||
|
||||
auto font = QFont();
|
||||
font.setPixelSize(int(_textFontSize));
|
||||
font.setWeight(QFont::DemiBold);
|
||||
proxy->setFont(font);
|
||||
|
||||
{
|
||||
auto option = proxy->document()->defaultTextOption();
|
||||
option.setAlignment(Qt::AlignCenter);
|
||||
proxy->document()->setDefaultTextOption(option);
|
||||
}
|
||||
|
||||
const auto shortSide = std::min(
|
||||
sceneRect().width(),
|
||||
sceneRect().height());
|
||||
const auto padding = int(_textFontSize * 0.4);
|
||||
const auto textWidth = int(shortSide * 0.8) - 2 * padding;
|
||||
proxy->setTextWidth(textWidth);
|
||||
const auto center = sceneRect().center();
|
||||
proxy->setPos(center.x() - textWidth / 2., center.y());
|
||||
|
||||
QGraphicsScene::addItem(proxy);
|
||||
proxy->setZValue((*_lastZ)++);
|
||||
proxy->setFocus();
|
||||
|
||||
proxy->onFinish = crl::guard(this, [=] {
|
||||
finishTextEditing(true);
|
||||
});
|
||||
proxy->onCancel = crl::guard(this, [=] {
|
||||
finishTextEditing(false);
|
||||
});
|
||||
|
||||
_textEdit = { .item = nullptr, .proxy = proxy };
|
||||
}
|
||||
|
||||
void Scene::startTextEditing(ItemText *item) {
|
||||
if (_textEdit.proxy) {
|
||||
finishTextEditing(true);
|
||||
}
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelDrawing();
|
||||
|
||||
auto *proxy = new TextEditProxy();
|
||||
proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
proxy->setDefaultTextColor(item->color());
|
||||
|
||||
auto font = QFont();
|
||||
font.setPixelSize(int(item->fontSize()));
|
||||
font.setWeight(QFont::DemiBold);
|
||||
proxy->setFont(font);
|
||||
|
||||
{
|
||||
auto option = proxy->document()->defaultTextOption();
|
||||
option.setAlignment(Qt::AlignCenter);
|
||||
proxy->document()->setDefaultTextOption(option);
|
||||
}
|
||||
|
||||
proxy->setPlainText(item->text());
|
||||
|
||||
const auto shortSide = std::min(
|
||||
sceneRect().width(),
|
||||
sceneRect().height());
|
||||
const auto padding = int(item->fontSize() * 0.4);
|
||||
const auto textWidth = int(shortSide * 0.8) - 2 * padding;
|
||||
proxy->setTextWidth(textWidth);
|
||||
|
||||
const auto scale = item->editScale();
|
||||
const auto proxyRect = proxy->boundingRect();
|
||||
const auto center = proxyRect.center();
|
||||
proxy->setTransformOriginPoint(center);
|
||||
proxy->setPos(item->scenePos() - center);
|
||||
proxy->setRotation(item->rotation());
|
||||
if (std::abs(scale - 1.) > 0.01) {
|
||||
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);
|
||||
|
||||
proxy->onFinish = crl::guard(this, [=] {
|
||||
finishTextEditing(true);
|
||||
});
|
||||
proxy->onCancel = crl::guard(this, [=] {
|
||||
finishTextEditing(false);
|
||||
});
|
||||
|
||||
_textEdit = { .item = item, .proxy = proxy };
|
||||
}
|
||||
|
||||
void Scene::finishTextEditing(bool save) {
|
||||
if (!_textEdit.proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto text = save
|
||||
? _textEdit.proxy->toPlainText().trimmed()
|
||||
: QString();
|
||||
const auto proxyRect = _textEdit.proxy->boundingRect();
|
||||
const auto proxyCenter = _textEdit.proxy->pos()
|
||||
+ QPointF(proxyRect.width() / 2., proxyRect.height() / 2.);
|
||||
auto *existingItem = _textEdit.item;
|
||||
|
||||
const auto proxy = static_cast<TextEditProxy*>(_textEdit.proxy);
|
||||
proxy->onFinish = nullptr;
|
||||
proxy->onCancel = nullptr;
|
||||
QGraphicsScene::removeItem(proxy);
|
||||
delete proxy;
|
||||
_textEdit.proxy = nullptr;
|
||||
_textEdit.item = nullptr;
|
||||
|
||||
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);
|
||||
const auto size = std::max(contentSize.width(), 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,
|
||||
TextStyle::Plain,
|
||||
imageSize,
|
||||
std::move(data));
|
||||
addItem(item);
|
||||
}
|
||||
} else if (existingItem) {
|
||||
if (save) {
|
||||
removeItem(existingItem);
|
||||
} else {
|
||||
existingItem->setVisible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scene::~Scene() {
|
||||
if (_textEdit.proxy) {
|
||||
const auto proxy = static_cast<TextEditProxy*>(_textEdit.proxy);
|
||||
proxy->onFinish = nullptr;
|
||||
proxy->onCancel = nullptr;
|
||||
QGraphicsScene::removeItem(proxy);
|
||||
delete proxy;
|
||||
_textEdit.proxy = nullptr;
|
||||
}
|
||||
// Prevent destroying by scene of all items.
|
||||
QGraphicsScene::removeItem(_canvas.get());
|
||||
for (const auto &item : items()) {
|
||||
|
||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include <QGraphicsScene>
|
||||
|
||||
class QGraphicsSceneMouseEvent;
|
||||
class QGraphicsTextItem;
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
@@ -21,6 +22,7 @@ class RpWidget;
|
||||
namespace Editor {
|
||||
|
||||
class ItemCanvas;
|
||||
class ItemText;
|
||||
class NumberedItem;
|
||||
|
||||
class Scene final : public QGraphicsScene {
|
||||
@@ -31,6 +33,7 @@ public:
|
||||
~Scene();
|
||||
void applyBrush(const QColor &color, float size, Brush::Tool tool);
|
||||
void setBlurSource(Fn<QImage(QRect)> source);
|
||||
void setTextDefaults(const QColor &color, float fontSize, int style);
|
||||
|
||||
[[nodiscard]] std::vector<ItemPtr> items(
|
||||
Qt::SortOrder order = Qt::DescendingOrder) const;
|
||||
@@ -46,6 +49,9 @@ public:
|
||||
|
||||
void cancelDrawing();
|
||||
|
||||
void startTextEditing(ItemText *item);
|
||||
void createTextAtCenter();
|
||||
|
||||
[[nodiscard]] bool hasUndo() const;
|
||||
[[nodiscard]] bool hasRedo() const;
|
||||
|
||||
@@ -62,6 +68,8 @@ protected:
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
private:
|
||||
void removeIf(Fn<bool(const ItemPtr &)> proj);
|
||||
void finishTextEditing(bool save);
|
||||
|
||||
const std::shared_ptr<ItemCanvas> _canvas;
|
||||
const std::shared_ptr<float64> _lastZ;
|
||||
Fn<QImage(QRect)> _blurSource;
|
||||
@@ -70,8 +78,18 @@ private:
|
||||
std::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;
|
||||
|
||||
float64 _lastLineZ = 0.;
|
||||
float64 _currentZoom = 1.;
|
||||
int _itemNumber = 0;
|
||||
|
||||
QColor _textColor;
|
||||
float _textFontSize = 0.f;
|
||||
int _textStyle = 3;
|
||||
|
||||
struct {
|
||||
ItemText *item = nullptr;
|
||||
QGraphicsTextItem *proxy = nullptr;
|
||||
} _textEdit;
|
||||
|
||||
rpl::event_stream<> _addsItem, _removesItem;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
|
||||
391
Telegram/SourceFiles/editor/scene/scene_item_text.cpp
Normal file
391
Telegram/SourceFiles/editor/scene/scene_item_text.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
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_item_text.h"
|
||||
|
||||
#include "editor/scene/scene.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "styles/style_editor.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsSceneContextMenuEvent>
|
||||
#include <QTextLayout>
|
||||
#include <QTextOption>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kPaddingFactor = 0.4f;
|
||||
constexpr auto kMaxWidthFactor = 0.8f;
|
||||
constexpr auto kMinContentWidth = 20;
|
||||
|
||||
struct LayoutMetrics {
|
||||
int contentWidth = 0;
|
||||
int contentHeight = 0;
|
||||
int padding = 0;
|
||||
int textMaxWidth = 0;
|
||||
};
|
||||
|
||||
QFont TextFont(float fontSize) {
|
||||
auto font = QFont();
|
||||
font.setPixelSize(std::max(int(fontSize), 1));
|
||||
font.setWeight(QFont::DemiBold);
|
||||
return font;
|
||||
}
|
||||
|
||||
float ComputeBrightness(const QColor &color) {
|
||||
return (color.red() * 0.2126f
|
||||
+ color.green() * 0.7152f
|
||||
+ color.blue() * 0.0722f) / 255.f;
|
||||
}
|
||||
|
||||
LayoutMetrics ComputeMetrics(
|
||||
const QString &text,
|
||||
float fontSize,
|
||||
const QSize &imageSize) {
|
||||
const auto padding = int(fontSize * kPaddingFactor);
|
||||
const auto shortSide = std::min(imageSize.width(), imageSize.height());
|
||||
const auto textMaxWidth = int(shortSide * kMaxWidthFactor) - 2 * padding;
|
||||
|
||||
const auto font = TextFont(fontSize);
|
||||
|
||||
auto processedText = text;
|
||||
processedText.replace('\n', QChar::LineSeparator);
|
||||
|
||||
auto option = QTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
|
||||
auto layout = QTextLayout(processedText, font);
|
||||
layout.setTextOption(option);
|
||||
layout.beginLayout();
|
||||
|
||||
auto totalHeight = 0.;
|
||||
auto maxWidth = 0.;
|
||||
while (true) {
|
||||
auto line = layout.createLine();
|
||||
if (!line.isValid()) {
|
||||
break;
|
||||
}
|
||||
line.setLineWidth(textMaxWidth);
|
||||
line.setPosition(QPointF(0, totalHeight));
|
||||
totalHeight += line.height();
|
||||
maxWidth = std::max(maxWidth, double(line.naturalTextWidth()));
|
||||
}
|
||||
layout.endLayout();
|
||||
|
||||
return {
|
||||
.contentWidth = std::max(int(std::ceil(maxWidth)), kMinContentWidth),
|
||||
.contentHeight = int(std::ceil(totalHeight)),
|
||||
.padding = padding,
|
||||
.textMaxWidth = textMaxWidth,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemText::ItemText(
|
||||
const QString &text,
|
||||
const QColor &color,
|
||||
float fontSize,
|
||||
TextStyle style,
|
||||
const QSize &imageSize,
|
||||
ItemBase::Data data)
|
||||
: ItemBase(std::move(data))
|
||||
, _text(text)
|
||||
, _color(color)
|
||||
, _fontSize(fontSize)
|
||||
, _textStyle(style)
|
||||
, _imageSize(imageSize) {
|
||||
renderContent();
|
||||
}
|
||||
|
||||
void ItemText::renderContent() {
|
||||
if (_text.isEmpty()) {
|
||||
_pixmap = QPixmap();
|
||||
setAspectRatio(1.);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto m = ComputeMetrics(_text, _fontSize, _imageSize);
|
||||
const auto pixWidth = m.contentWidth + 2 * m.padding;
|
||||
const auto pixHeight = m.contentHeight + 2 * m.padding;
|
||||
|
||||
const auto font = TextFont(_fontSize);
|
||||
|
||||
auto processedText = _text;
|
||||
processedText.replace('\n', QChar::LineSeparator);
|
||||
|
||||
auto option = QTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
|
||||
auto layout = QTextLayout(processedText, font);
|
||||
layout.setTextOption(option);
|
||||
layout.beginLayout();
|
||||
auto y = 0.;
|
||||
while (true) {
|
||||
auto line = layout.createLine();
|
||||
if (!line.isValid()) {
|
||||
break;
|
||||
}
|
||||
line.setLineWidth(m.textMaxWidth);
|
||||
line.setPosition(QPointF(0, y));
|
||||
y += line.height();
|
||||
}
|
||||
layout.endLayout();
|
||||
|
||||
auto textColor = _color;
|
||||
auto bgColor = QColor(Qt::transparent);
|
||||
const auto brightness = ComputeBrightness(_color);
|
||||
const auto cornerRadius = _fontSize / 3.f;
|
||||
const auto hasPerLineBackground =
|
||||
(_textStyle == TextStyle::Framed)
|
||||
|| (_textStyle == TextStyle::SemiTransparent);
|
||||
|
||||
switch (_textStyle) {
|
||||
case TextStyle::Framed:
|
||||
bgColor = _color;
|
||||
textColor = (brightness >= 0.721f)
|
||||
? QColor(0, 0, 0)
|
||||
: QColor(255, 255, 255);
|
||||
break;
|
||||
case TextStyle::SemiTransparent:
|
||||
bgColor = (brightness >= 0.25f)
|
||||
? QColor(0, 0, 0, 0x99)
|
||||
: QColor(255, 255, 255, 0x99);
|
||||
break;
|
||||
case TextStyle::Plain:
|
||||
break;
|
||||
}
|
||||
|
||||
const auto dpr = style::DevicePixelRatio();
|
||||
auto pixmap = QPixmap(QSize(pixWidth, pixHeight) * dpr);
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
{
|
||||
auto p = QPainter(&pixmap);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
if (hasPerLineBackground) {
|
||||
const auto linePadH = _fontSize / 3.f;
|
||||
const auto linePadV = _fontSize / 8.f;
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(bgColor);
|
||||
|
||||
for (auto i = 0; i < layout.lineCount(); ++i) {
|
||||
const auto line = layout.lineAt(i);
|
||||
const auto natWidth = line.naturalTextWidth();
|
||||
const auto xOffset =
|
||||
(m.contentWidth - natWidth) / 2.;
|
||||
const auto rect = QRectF(
|
||||
m.padding + xOffset - linePadH,
|
||||
m.padding + line.y() - linePadV,
|
||||
natWidth + 2. * linePadH,
|
||||
line.height() + 2. * linePadV);
|
||||
p.drawRoundedRect(rect, cornerRadius, cornerRadius);
|
||||
}
|
||||
}
|
||||
|
||||
p.setPen(textColor);
|
||||
for (auto i = 0; i < layout.lineCount(); ++i) {
|
||||
const auto line = layout.lineAt(i);
|
||||
const auto xOffset =
|
||||
(m.contentWidth - line.naturalTextWidth()) / 2.;
|
||||
line.draw(&p, QPointF(m.padding + xOffset, m.padding));
|
||||
}
|
||||
}
|
||||
|
||||
_pixmap = std::move(pixmap);
|
||||
const auto handleMargin = std::max(
|
||||
innerRect().width() - contentRect().width(),
|
||||
0.);
|
||||
setAspectRatio(
|
||||
(pixHeight + handleMargin) / float64(pixWidth + handleMargin));
|
||||
}
|
||||
|
||||
QSize ItemText::computeContentSize(
|
||||
const QString &text,
|
||||
float fontSize,
|
||||
const QSize &imageSize) {
|
||||
if (text.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
auto processedText = text;
|
||||
processedText.replace('\n', QChar::LineSeparator);
|
||||
const auto m = ComputeMetrics(processedText, fontSize, imageSize);
|
||||
return QSize(
|
||||
m.contentWidth + 2 * m.padding,
|
||||
m.contentHeight + 2 * m.padding);
|
||||
}
|
||||
|
||||
void ItemText::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *w) {
|
||||
if (!_pixmap.isNull()) {
|
||||
const auto rect = contentRect();
|
||||
const auto pixmapSize = QSizeF(
|
||||
_pixmap.size() / style::DevicePixelRatio()
|
||||
).scaled(rect.size(), Qt::KeepAspectRatio);
|
||||
const auto resultRect = QRectF(
|
||||
rect.topLeft(),
|
||||
pixmapSize
|
||||
).translated(
|
||||
(rect.width() - pixmapSize.width()) / 2.,
|
||||
(rect.height() - pixmapSize.height()) / 2.);
|
||||
if (flipped()) {
|
||||
p->save();
|
||||
const auto center = resultRect.center();
|
||||
p->translate(center);
|
||||
p->scale(-1, 1);
|
||||
p->translate(-center);
|
||||
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||
p->restore();
|
||||
} else {
|
||||
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||
}
|
||||
}
|
||||
ItemBase::paint(p, option, w);
|
||||
}
|
||||
|
||||
int ItemText::type() const {
|
||||
return Type;
|
||||
}
|
||||
|
||||
const QString &ItemText::text() const {
|
||||
return _text;
|
||||
}
|
||||
|
||||
void ItemText::setText(const QString &text) {
|
||||
_text = text;
|
||||
renderContent();
|
||||
update();
|
||||
}
|
||||
|
||||
const QColor &ItemText::color() const {
|
||||
return _color;
|
||||
}
|
||||
|
||||
void ItemText::setColor(const QColor &color) {
|
||||
_color = color;
|
||||
renderContent();
|
||||
update();
|
||||
}
|
||||
|
||||
float ItemText::fontSize() const {
|
||||
return _fontSize;
|
||||
}
|
||||
|
||||
float64 ItemText::editScale() const {
|
||||
const auto natural = computeContentSize(_text, _fontSize, _imageSize);
|
||||
if (natural.width() <= 0) {
|
||||
return 1.;
|
||||
}
|
||||
return size() / natural.width();
|
||||
}
|
||||
|
||||
TextStyle ItemText::textStyle() const {
|
||||
return _textStyle;
|
||||
}
|
||||
|
||||
void ItemText::setTextStyle(TextStyle style) {
|
||||
_textStyle = style;
|
||||
renderContent();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemText::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
s->startTextEditing(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemText::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
|
||||
if (scene()) {
|
||||
scene()->clearSelection();
|
||||
setSelected(true);
|
||||
}
|
||||
|
||||
_contextMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||
nullptr,
|
||||
st::popupMenuWithIcons);
|
||||
const auto add = [&](const QString &text, TextStyle style) {
|
||||
const auto checked = (_textStyle == style);
|
||||
auto action = _contextMenu->addAction(text, [=] {
|
||||
setTextStyle(style);
|
||||
});
|
||||
if (checked) {
|
||||
action->setChecked(true);
|
||||
}
|
||||
};
|
||||
add(u"Plain"_q, TextStyle::Plain);
|
||||
add(u"Framed"_q, TextStyle::Framed);
|
||||
add(u"Semi-Transparent"_q, TextStyle::SemiTransparent);
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
_contextMenu->addAction(
|
||||
tr::lng_photo_editor_menu_delete(tr::now),
|
||||
[=] { actionDelete(); },
|
||||
&st::menuIconDelete);
|
||||
_contextMenu->addAction(
|
||||
tr::lng_photo_editor_menu_duplicate(tr::now),
|
||||
[=] { actionDuplicate(); },
|
||||
&st::menuIconCopy);
|
||||
|
||||
_contextMenu->popup(event->screenPos());
|
||||
}
|
||||
|
||||
void ItemText::performFlip() {
|
||||
update();
|
||||
}
|
||||
|
||||
std::shared_ptr<ItemBase> ItemText::duplicate(ItemBase::Data data) const {
|
||||
return std::make_shared<ItemText>(
|
||||
_text,
|
||||
_color,
|
||||
_fontSize,
|
||||
_textStyle,
|
||||
_imageSize,
|
||||
std::move(data));
|
||||
}
|
||||
|
||||
void ItemText::save(SaveState state) {
|
||||
ItemBase::save(state);
|
||||
auto &saved = (state == SaveState::Keep) ? _keepedState : _savedState;
|
||||
saved = {
|
||||
.saved = true,
|
||||
.status = status(),
|
||||
.text = _text,
|
||||
.color = _color,
|
||||
.fontSize = _fontSize,
|
||||
.textStyle = _textStyle,
|
||||
};
|
||||
}
|
||||
|
||||
void ItemText::restore(SaveState state) {
|
||||
if (!hasState(state)) {
|
||||
return;
|
||||
}
|
||||
const auto &saved = (state == SaveState::Keep) ? _keepedState : _savedState;
|
||||
_text = saved.text;
|
||||
_color = saved.color;
|
||||
_fontSize = saved.fontSize;
|
||||
_textStyle = saved.textStyle;
|
||||
renderContent();
|
||||
ItemBase::restore(state);
|
||||
}
|
||||
|
||||
bool ItemText::hasState(SaveState state) const {
|
||||
return ItemBase::hasState(state);
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
93
Telegram/SourceFiles/editor/scene/scene_item_text.h
Normal file
93
Telegram/SourceFiles/editor/scene/scene_item_text.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
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/scene/scene_item_base.h"
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
enum class TextStyle : uchar {
|
||||
Framed,
|
||||
SemiTransparent,
|
||||
Plain,
|
||||
};
|
||||
|
||||
class ItemText : public ItemBase {
|
||||
public:
|
||||
enum { Type = ItemBase::Type + 2 };
|
||||
|
||||
ItemText(
|
||||
const QString &text,
|
||||
const QColor &color,
|
||||
float fontSize,
|
||||
TextStyle style,
|
||||
const QSize &imageSize,
|
||||
ItemBase::Data data);
|
||||
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
int type() const override;
|
||||
|
||||
[[nodiscard]] const QString &text() const;
|
||||
void setText(const QString &text);
|
||||
|
||||
[[nodiscard]] const QColor &color() const;
|
||||
void setColor(const QColor &color);
|
||||
|
||||
[[nodiscard]] float fontSize() const;
|
||||
|
||||
[[nodiscard]] TextStyle textStyle() const;
|
||||
void setTextStyle(TextStyle style);
|
||||
|
||||
[[nodiscard]] float64 editScale() const;
|
||||
|
||||
[[nodiscard]] static QSize computeContentSize(
|
||||
const QString &text,
|
||||
float fontSize,
|
||||
const QSize &imageSize);
|
||||
|
||||
void save(SaveState state) override;
|
||||
void restore(SaveState state) override;
|
||||
bool hasState(SaveState state) const override;
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void performFlip() override;
|
||||
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||
|
||||
private:
|
||||
void renderContent();
|
||||
|
||||
QString _text;
|
||||
QColor _color;
|
||||
float _fontSize;
|
||||
TextStyle _textStyle = TextStyle::Plain;
|
||||
QSize _imageSize;
|
||||
QPixmap _pixmap;
|
||||
base::unique_qptr<Ui::PopupMenu> _contextMenu;
|
||||
|
||||
struct SavedText {
|
||||
bool saved = false;
|
||||
NumberedItem::Status status = NumberedItem::Status::Normal;
|
||||
QString text;
|
||||
QColor color;
|
||||
float fontSize = 0;
|
||||
TextStyle textStyle = TextStyle::Plain;
|
||||
};
|
||||
SavedText _savedState, _keepedState;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
@@ -147,6 +147,8 @@ PRIVATE
|
||||
editor/scene/scene_item_image.h
|
||||
editor/scene/scene_item_line.cpp
|
||||
editor/scene/scene_item_line.h
|
||||
editor/scene/scene_item_text.cpp
|
||||
editor/scene/scene_item_text.h
|
||||
|
||||
ui/boxes/about_cocoon_box.h
|
||||
ui/boxes/about_cocoon_box.cpp
|
||||
|
||||
Reference in New Issue
Block a user