init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
220
Telegram/SourceFiles/editor/scene/scene.cpp
Normal file
220
Telegram/SourceFiles/editor/scene/scene.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
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 "ui/rp_widget.h"
|
||||
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
using ItemPtr = std::shared_ptr<NumberedItem>;
|
||||
|
||||
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
|
||||
return event->isAccepted() || (event->button() == Qt::RightButton);
|
||||
}
|
||||
|
||||
} // 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) {
|
||||
const auto item = std::make_shared<ItemLine>(
|
||||
std::move(content.pixmap));
|
||||
item->setPos(content.position);
|
||||
addItem(item);
|
||||
_canvas->setZValue(++_lastLineZ);
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Scene::cancelDrawing() {
|
||||
_canvas->cancelDrawing();
|
||||
}
|
||||
|
||||
void Scene::addItem(ItemPtr item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
item->setNumber(_itemNumber++);
|
||||
QGraphicsScene::addItem(item.get());
|
||||
_items.push_back(std::move(item));
|
||||
_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) {
|
||||
QGraphicsScene::mousePressEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMousePressEvent(event);
|
||||
}
|
||||
|
||||
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseReleaseEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
QGraphicsScene::mouseMoveEvent(event);
|
||||
if (SkipMouseEvent(event)) {
|
||||
return;
|
||||
}
|
||||
_canvas->handleMouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void Scene::applyBrush(const QColor &color, float size) {
|
||||
_canvas->applyBrush(color, size);
|
||||
}
|
||||
|
||||
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) {
|
||||
_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()) {
|
||||
(*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()) {
|
||||
(*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);
|
||||
}
|
||||
|
||||
void Scene::clearRedoList() {
|
||||
for (const auto &item : _items) {
|
||||
if (item->isUndidStatus()) {
|
||||
item->setStatus(NumberedItem::Status::Removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::save(SaveState state) {
|
||||
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();
|
||||
}
|
||||
|
||||
Scene::~Scene() {
|
||||
// Prevent destroying by scene of all items.
|
||||
QGraphicsScene::removeItem(_canvas.get());
|
||||
for (const auto &item : items()) {
|
||||
// Scene loses ownership of an item.
|
||||
QGraphicsScene::removeItem(item.get());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
77
Telegram/SourceFiles/editor/scene/scene.h
Normal file
77
Telegram/SourceFiles/editor/scene/scene.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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>
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
||||
class QGraphicsSceneMouseEvent;
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemCanvas;
|
||||
class NumberedItem;
|
||||
|
||||
class Scene final : public QGraphicsScene {
|
||||
public:
|
||||
using ItemPtr = std::shared_ptr<NumberedItem>;
|
||||
|
||||
Scene(const QRectF &rect);
|
||||
~Scene();
|
||||
void applyBrush(const QColor &color, float size);
|
||||
|
||||
[[nodiscard]] std::vector<ItemPtr> items(
|
||||
Qt::SortOrder order = Qt::DescendingOrder) const;
|
||||
void addItem(ItemPtr item);
|
||||
void removeItem(not_null<QGraphicsItem*> item);
|
||||
void removeItem(const ItemPtr &item);
|
||||
[[nodiscard]] rpl::producer<> addsItem() const;
|
||||
[[nodiscard]] rpl::producer<> removesItem() const;
|
||||
|
||||
[[nodiscard]] std::shared_ptr<float64> lastZ() const;
|
||||
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
void cancelDrawing();
|
||||
|
||||
[[nodiscard]] bool hasUndo() const;
|
||||
[[nodiscard]] bool hasRedo() const;
|
||||
|
||||
void performUndo();
|
||||
void performRedo();
|
||||
|
||||
void save(SaveState state);
|
||||
void restore(SaveState state);
|
||||
|
||||
void clearRedoList();
|
||||
protected:
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
private:
|
||||
void removeIf(Fn<bool(const ItemPtr &)> proj);
|
||||
const std::shared_ptr<ItemCanvas> _canvas;
|
||||
const std::shared_ptr<float64> _lastZ;
|
||||
|
||||
std::vector<ItemPtr> _items;
|
||||
|
||||
float64 _lastLineZ = 0.;
|
||||
int _itemNumber = 0;
|
||||
|
||||
rpl::event_stream<> _addsItem, _removesItem;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
456
Telegram/SourceFiles/editor/scene/scene_item_base.cpp
Normal file
456
Telegram/SourceFiles/editor/scene/scene_item_base.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
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_base.h"
|
||||
|
||||
#include "editor/scene/scene.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_editor.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneHoverEvent>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
#include <QtMath>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSnapAngle = 45.;
|
||||
|
||||
const auto kDuplicateSequence = QKeySequence("ctrl+d");
|
||||
const auto kFlipSequence = QKeySequence("ctrl+s");
|
||||
const auto kDeleteSequence = QKeySequence("delete");
|
||||
|
||||
constexpr auto kMinSizeRatio = 0.05;
|
||||
constexpr auto kMaxSizeRatio = 1.00;
|
||||
|
||||
auto Normalized(float64 angle) {
|
||||
return angle
|
||||
+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int NumberedItem::type() const {
|
||||
return NumberedItem::Type;
|
||||
}
|
||||
|
||||
int NumberedItem::number() const {
|
||||
return _number;
|
||||
}
|
||||
|
||||
void NumberedItem::setNumber(int number) {
|
||||
_number = number;
|
||||
}
|
||||
|
||||
NumberedItem::Status NumberedItem::status() const {
|
||||
return _status;
|
||||
}
|
||||
|
||||
bool NumberedItem::isNormalStatus() const {
|
||||
return _status == Status::Normal;
|
||||
}
|
||||
|
||||
bool NumberedItem::isUndidStatus() const {
|
||||
return _status == Status::Undid;
|
||||
}
|
||||
|
||||
bool NumberedItem::isRemovedStatus() const {
|
||||
return _status == Status::Removed;
|
||||
}
|
||||
|
||||
void NumberedItem::save(SaveState state) {
|
||||
}
|
||||
|
||||
void NumberedItem::restore(SaveState state) {
|
||||
}
|
||||
|
||||
bool NumberedItem::hasState(SaveState state) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void NumberedItem::setStatus(Status status) {
|
||||
if (status != _status) {
|
||||
_status = status;
|
||||
setVisible(status == Status::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
ItemBase::ItemBase(Data data)
|
||||
: _lastZ(data.zPtr)
|
||||
, _imageSize(data.imageSize)
|
||||
, _horizontalSize(data.size) {
|
||||
setFlags(QGraphicsItem::ItemIsMovable
|
||||
| QGraphicsItem::ItemIsSelectable
|
||||
| QGraphicsItem::ItemIsFocusable);
|
||||
setAcceptHoverEvents(true);
|
||||
applyData(data);
|
||||
}
|
||||
|
||||
QRectF ItemBase::boundingRect() const {
|
||||
return innerRect() + _scaledInnerMargins;
|
||||
}
|
||||
|
||||
QRectF ItemBase::contentRect() const {
|
||||
return innerRect() - _scaledInnerMargins;
|
||||
}
|
||||
|
||||
QRectF ItemBase::innerRect() const {
|
||||
const auto &hSize = _horizontalSize;
|
||||
const auto &vSize = _verticalSize;
|
||||
return QRectF(-hSize / 2, -vSize / 2, hSize, vSize);
|
||||
}
|
||||
|
||||
void ItemBase::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *) {
|
||||
if (!(option->state & QStyle::State_Selected)) {
|
||||
return;
|
||||
}
|
||||
PainterHighQualityEnabler hq(*p);
|
||||
const auto hasFocus = (option->state & QStyle::State_HasFocus);
|
||||
p->setPen(hasFocus ? _pens.select : _pens.selectInactive);
|
||||
p->drawRect(innerRect());
|
||||
|
||||
p->setPen(hasFocus ? _pens.handle : _pens.handleInactive);
|
||||
p->setBrush(st::photoEditorItemBaseHandleFg);
|
||||
p->drawEllipse(rightHandleRect());
|
||||
p->drawEllipse(leftHandleRect());
|
||||
}
|
||||
|
||||
void ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if (isHandling()) {
|
||||
const auto mousePos = event->pos();
|
||||
const auto shift = event->modifiers().testFlag(Qt::ShiftModifier);
|
||||
const auto isLeft = (_handle == HandleType::Left);
|
||||
if (!shift) {
|
||||
// Resize.
|
||||
const auto p = isLeft ? (mousePos * -1) : mousePos;
|
||||
const auto dx = int(2.0 * p.x());
|
||||
const auto dy = int(2.0 * p.y());
|
||||
prepareGeometryChange();
|
||||
_horizontalSize = std::clamp(
|
||||
(dx > dy ? dx : dy),
|
||||
_sizeLimits.min,
|
||||
_sizeLimits.max);
|
||||
updateVerticalSize();
|
||||
}
|
||||
|
||||
// Rotate.
|
||||
const auto origin = mapToScene(boundingRect().center());
|
||||
const auto pos = mapToScene(mousePos);
|
||||
|
||||
const auto diff = pos - origin;
|
||||
const auto angle = Normalized((isLeft ? 180 : 0)
|
||||
+ (std::atan2(diff.y(), diff.x()) * 180 / M_PI));
|
||||
setRotation(shift
|
||||
? (base::SafeRound(angle / kSnapAngle) * kSnapAngle)
|
||||
: angle);
|
||||
} else {
|
||||
QGraphicsItem::mouseMoveEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
|
||||
setCursor(isHandling()
|
||||
? Qt::ClosedHandCursor
|
||||
: (handleType(event->pos()) != HandleType::None) && isSelected()
|
||||
? Qt::OpenHandCursor
|
||||
: Qt::ArrowCursor);
|
||||
QGraphicsItem::hoverMoveEvent(event);
|
||||
}
|
||||
|
||||
void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
||||
setZValue((*_lastZ)++);
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
_handle = handleType(event->pos());
|
||||
}
|
||||
if (isHandling()) {
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
} else {
|
||||
QGraphicsItem::mousePressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||
if ((event->button() == Qt::LeftButton) && isHandling()) {
|
||||
_handle = HandleType::None;
|
||||
} else {
|
||||
QGraphicsItem::mouseReleaseEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
|
||||
if (scene()) {
|
||||
scene()->clearSelection();
|
||||
setSelected(true);
|
||||
}
|
||||
|
||||
const auto add = [&](
|
||||
auto base,
|
||||
const QKeySequence &sequence,
|
||||
Fn<void()> callback,
|
||||
const style::icon *icon) {
|
||||
// TODO: refactor.
|
||||
const auto sequenceText = QChar('\t')
|
||||
+ sequence.toString(QKeySequence::NativeText);
|
||||
_menu->addAction(
|
||||
base(tr::now) + sequenceText,
|
||||
std::move(callback),
|
||||
icon);
|
||||
};
|
||||
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
nullptr,
|
||||
st::popupMenuWithIcons);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_delete,
|
||||
kDeleteSequence,
|
||||
[=] { actionDelete(); },
|
||||
&st::menuIconDelete);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_flip,
|
||||
kFlipSequence,
|
||||
[=] { actionFlip(); },
|
||||
&st::menuIconFlip);
|
||||
add(
|
||||
tr::lng_photo_editor_menu_duplicate,
|
||||
kDuplicateSequence,
|
||||
[=] { actionDuplicate(); },
|
||||
&st::menuIconCopy);
|
||||
|
||||
_menu->popup(event->screenPos());
|
||||
}
|
||||
|
||||
void ItemBase::performForSelectedItems(Action action) {
|
||||
if (const auto s = scene()) {
|
||||
for (const auto item : s->selectedItems()) {
|
||||
if (const auto base = static_cast<ItemBase*>(item)) {
|
||||
(base->*action)();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::actionFlip() {
|
||||
setFlip(!flipped());
|
||||
}
|
||||
|
||||
void ItemBase::actionDelete() {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
s->removeItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::actionDuplicate() {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
auto data = generateData();
|
||||
data.x += int(_horizontalSize / 3);
|
||||
data.y += int(_verticalSize / 3);
|
||||
const auto newItem = duplicate(std::move(data));
|
||||
if (hasFocus()) {
|
||||
newItem->setFocus();
|
||||
}
|
||||
const auto selected = isSelected();
|
||||
newItem->setSelected(selected);
|
||||
setSelected(false);
|
||||
s->addItem(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Escape) {
|
||||
if (const auto s = scene()) {
|
||||
s->clearSelection();
|
||||
s->clearFocus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleActionKey(e);
|
||||
}
|
||||
|
||||
void ItemBase::handleActionKey(not_null<QKeyEvent*> e) {
|
||||
const auto matches = [&](const QKeySequence &sequence) {
|
||||
const auto searchKey = (e->modifiers() | e->key())
|
||||
& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
|
||||
const auto events = QKeySequence(searchKey);
|
||||
return sequence.matches(events) == QKeySequence::ExactMatch;
|
||||
};
|
||||
if (matches(kDuplicateSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionDuplicate);
|
||||
} else if (matches(kDeleteSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionDelete);
|
||||
} else if (matches(kFlipSequence)) {
|
||||
performForSelectedItems(&ItemBase::actionFlip);
|
||||
}
|
||||
}
|
||||
|
||||
QRectF ItemBase::rightHandleRect() const {
|
||||
return QRectF(
|
||||
(_horizontalSize / 2) - (_scaledHandleSize / 2),
|
||||
0 - (_scaledHandleSize / 2),
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize);
|
||||
}
|
||||
|
||||
QRectF ItemBase::leftHandleRect() const {
|
||||
return QRectF(
|
||||
(-_horizontalSize / 2) - (_scaledHandleSize / 2),
|
||||
0 - (_scaledHandleSize / 2),
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize);
|
||||
}
|
||||
|
||||
bool ItemBase::isHandling() const {
|
||||
return _handle != HandleType::None;
|
||||
}
|
||||
|
||||
float64 ItemBase::size() const {
|
||||
return _horizontalSize;
|
||||
}
|
||||
|
||||
void ItemBase::updateVerticalSize() {
|
||||
const auto verticalSize = _horizontalSize * _aspectRatio;
|
||||
_verticalSize = std::max(
|
||||
verticalSize,
|
||||
float64(_sizeLimits.min));
|
||||
if (verticalSize < _sizeLimits.min) {
|
||||
_horizontalSize = _verticalSize / _aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::setAspectRatio(float64 aspectRatio) {
|
||||
_aspectRatio = aspectRatio;
|
||||
updateVerticalSize();
|
||||
}
|
||||
|
||||
ItemBase::HandleType ItemBase::handleType(const QPointF &pos) const {
|
||||
return rightHandleRect().contains(pos)
|
||||
? HandleType::Right
|
||||
: leftHandleRect().contains(pos)
|
||||
? HandleType::Left
|
||||
: HandleType::None;
|
||||
}
|
||||
|
||||
bool ItemBase::flipped() const {
|
||||
return _flipped;
|
||||
}
|
||||
|
||||
void ItemBase::setFlip(bool value) {
|
||||
if (_flipped != value) {
|
||||
performFlip();
|
||||
_flipped = value;
|
||||
}
|
||||
}
|
||||
|
||||
int ItemBase::type() const {
|
||||
return ItemBase::Type;
|
||||
}
|
||||
|
||||
void ItemBase::updateZoom(float64 zoom) {
|
||||
_scaledHandleSize = st::photoEditorItemHandleSize / zoom;
|
||||
_scaledInnerMargins = QMarginsF(
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize,
|
||||
_scaledHandleSize) * 0.5;
|
||||
|
||||
const auto maxSide = std::max(
|
||||
_imageSize.width(),
|
||||
_imageSize.height());
|
||||
_sizeLimits = {
|
||||
.min = int(maxSide * kMinSizeRatio),
|
||||
.max = int(maxSide * kMaxSizeRatio),
|
||||
};
|
||||
_horizontalSize = std::clamp(
|
||||
_horizontalSize,
|
||||
float64(_sizeLimits.min),
|
||||
float64(_sizeLimits.max));
|
||||
updateVerticalSize();
|
||||
|
||||
updatePens(QPen(
|
||||
QBrush(),
|
||||
1 / zoom,
|
||||
Qt::DashLine,
|
||||
Qt::SquareCap,
|
||||
Qt::RoundJoin));
|
||||
}
|
||||
|
||||
void ItemBase::performFlip() {
|
||||
}
|
||||
|
||||
void ItemBase::updatePens(QPen pen) {
|
||||
_pens = {
|
||||
.select = pen,
|
||||
.selectInactive = pen,
|
||||
.handle = pen,
|
||||
.handleInactive = pen,
|
||||
};
|
||||
_pens.select.setColor(Qt::white);
|
||||
_pens.selectInactive.setColor(Qt::gray);
|
||||
_pens.handle.setColor(Qt::white);
|
||||
_pens.handleInactive.setColor(Qt::gray);
|
||||
_pens.handle.setStyle(Qt::SolidLine);
|
||||
_pens.handleInactive.setStyle(Qt::SolidLine);
|
||||
}
|
||||
|
||||
ItemBase::Data ItemBase::generateData() const {
|
||||
return {
|
||||
.initialZoom = (st::photoEditorItemHandleSize / _scaledHandleSize),
|
||||
.zPtr = _lastZ,
|
||||
.size = int(_horizontalSize),
|
||||
.x = int(scenePos().x()),
|
||||
.y = int(scenePos().y()),
|
||||
.flipped = flipped(),
|
||||
.rotation = int(rotation()),
|
||||
.imageSize = _imageSize,
|
||||
};
|
||||
}
|
||||
|
||||
void ItemBase::applyData(const Data &data) {
|
||||
// _lastZ is const.
|
||||
// _imageSize is const.
|
||||
_horizontalSize = data.size;
|
||||
setPos(data.x, data.y);
|
||||
setZValue((*_lastZ)++);
|
||||
setFlip(data.flipped);
|
||||
setRotation(data.rotation);
|
||||
updateZoom(data.initialZoom);
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemBase::save(SaveState state) {
|
||||
const auto z = zValue();
|
||||
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
saved = {
|
||||
.data = generateData(),
|
||||
.zValue = z,
|
||||
.status = status(),
|
||||
};
|
||||
}
|
||||
|
||||
void ItemBase::restore(SaveState state) {
|
||||
if (!hasState(state)) {
|
||||
return;
|
||||
}
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
applyData(saved.data);
|
||||
setZValue(saved.zValue);
|
||||
setStatus(saved.status);
|
||||
}
|
||||
|
||||
bool ItemBase::hasState(SaveState state) const {
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
return saved.zValue;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
159
Telegram/SourceFiles/editor/scene/scene_item_base.h
Normal file
159
Telegram/SourceFiles/editor/scene/scene_item_base.h
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
#include <QGraphicsItem>
|
||||
|
||||
class QGraphicsSceneHoverEvent;
|
||||
class QGraphicsSceneMouseEvent;
|
||||
class QStyleOptionGraphicsItem;
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class NumberedItem : public QGraphicsItem {
|
||||
public:
|
||||
enum class Status {
|
||||
Normal,
|
||||
Undid,
|
||||
Removed,
|
||||
};
|
||||
|
||||
enum { Type = UserType + 1 };
|
||||
using QGraphicsItem::QGraphicsItem;
|
||||
|
||||
int type() const override;
|
||||
void setNumber(int number);
|
||||
[[nodiscard]] int number() const;
|
||||
|
||||
[[nodiscard]] Status status() const;
|
||||
void setStatus(Status status);
|
||||
[[nodiscard]] bool isNormalStatus() const;
|
||||
[[nodiscard]] bool isUndidStatus() const;
|
||||
[[nodiscard]] bool isRemovedStatus() const;
|
||||
|
||||
virtual void save(SaveState state);
|
||||
virtual void restore(SaveState state);
|
||||
virtual bool hasState(SaveState state) const;
|
||||
private:
|
||||
int _number = 0;
|
||||
Status _status = Status::Normal;
|
||||
};
|
||||
|
||||
class ItemBase : public NumberedItem {
|
||||
public:
|
||||
enum { Type = UserType + 2 };
|
||||
|
||||
struct Data {
|
||||
float64 initialZoom = 0.;
|
||||
std::shared_ptr<float64> zPtr;
|
||||
int size = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool flipped = false;
|
||||
int rotation = 0;
|
||||
QSize imageSize;
|
||||
};
|
||||
|
||||
ItemBase(Data data);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
int type() const override;
|
||||
|
||||
bool flipped() const;
|
||||
void setFlip(bool value);
|
||||
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
bool hasState(SaveState state) const override;
|
||||
void save(SaveState state) override;
|
||||
void restore(SaveState state) override;
|
||||
protected:
|
||||
enum HandleType {
|
||||
None,
|
||||
Left,
|
||||
Right,
|
||||
};
|
||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
|
||||
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
|
||||
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
using Action = void(ItemBase::*)();
|
||||
void performForSelectedItems(Action action);
|
||||
void actionFlip();
|
||||
void actionDelete();
|
||||
void actionDuplicate();
|
||||
|
||||
QRectF contentRect() const;
|
||||
QRectF innerRect() const;
|
||||
float64 size() const;
|
||||
float64 horizontalSize() const;
|
||||
float64 verticalSize() const;
|
||||
void setAspectRatio(float64 aspectRatio);
|
||||
|
||||
virtual void performFlip();
|
||||
virtual std::shared_ptr<ItemBase> duplicate(Data data) const = 0;
|
||||
private:
|
||||
HandleType handleType(const QPointF &pos) const;
|
||||
QRectF rightHandleRect() const;
|
||||
QRectF leftHandleRect() const;
|
||||
bool isHandling() const;
|
||||
void updateVerticalSize();
|
||||
void updatePens(QPen pen);
|
||||
void handleActionKey(not_null<QKeyEvent*> e);
|
||||
|
||||
Data generateData() const;
|
||||
void applyData(const Data &data);
|
||||
|
||||
const std::shared_ptr<float64> _lastZ;
|
||||
const QSize _imageSize;
|
||||
|
||||
struct {
|
||||
QPen select;
|
||||
QPen selectInactive;
|
||||
QPen handle;
|
||||
QPen handleInactive;
|
||||
} _pens;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
struct {
|
||||
Data data;
|
||||
float64 zValue = 0.;
|
||||
NumberedItem::Status status;
|
||||
} _saved, _keeped;
|
||||
|
||||
struct {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
} _sizeLimits;
|
||||
float64 _scaledHandleSize = 1.0;
|
||||
QMarginsF _scaledInnerMargins;
|
||||
|
||||
float64 _horizontalSize = 0;
|
||||
float64 _verticalSize = 0;
|
||||
float64 _aspectRatio = 1.0;
|
||||
HandleType _handle = HandleType::None;
|
||||
|
||||
bool _flipped = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
324
Telegram/SourceFiles/editor/scene/scene_item_canvas.cpp
Normal file
324
Telegram/SourceFiles/editor/scene/scene_item_canvas.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
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_canvas.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinPointDistanceBase = 2.0;
|
||||
constexpr auto kMaxPointDistance = 15.0;
|
||||
constexpr auto kSmoothingStrength = 0.5;
|
||||
constexpr auto kPressureDecay = 0.95;
|
||||
constexpr auto kMinPressure = 0.3;
|
||||
constexpr auto kBatchUpdateInterval = 16;
|
||||
constexpr auto kSegmentOverlap = 3;
|
||||
constexpr auto kHalfStrength = kSmoothingStrength / 2.0;
|
||||
constexpr auto kInvStrength = 1.0 - kSmoothingStrength;
|
||||
|
||||
[[nodiscard]] float64 PointDistance(const QPointF &a, const QPointF &b) {
|
||||
const auto dx = a.x() - b.x();
|
||||
const auto dy = a.y() - b.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemCanvas::ItemCanvas() {
|
||||
setAcceptedMouseButtons({});
|
||||
}
|
||||
|
||||
void ItemCanvas::clearPixmap() {
|
||||
_hq = nullptr;
|
||||
_p = nullptr;
|
||||
|
||||
_pixmap = QPixmap(
|
||||
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
|
||||
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_pixmap.fill(Qt::transparent);
|
||||
|
||||
_p = std::make_unique<Painter>(&_pixmap);
|
||||
_hq = std::make_unique<PainterHighQualityEnabler>(*_p);
|
||||
_p->setPen(Qt::NoPen);
|
||||
_p->setBrush(_brushData.color);
|
||||
}
|
||||
|
||||
void ItemCanvas::applyBrush(const QColor &color, float size) {
|
||||
_brushData.color = color;
|
||||
_brushData.size = size;
|
||||
_p->setBrush(color);
|
||||
_brushMargins = QMarginsF(size, size, size, size);// / 2.;
|
||||
}
|
||||
|
||||
QRectF ItemCanvas::boundingRect() const {
|
||||
return scene()->sceneRect();
|
||||
}
|
||||
|
||||
void ItemCanvas::computeContentRect(const QPointF &p) {
|
||||
if (!scene()) {
|
||||
return;
|
||||
}
|
||||
const auto sceneSize = scene()->sceneRect().size();
|
||||
_contentRect = QRectF(
|
||||
QPointF(
|
||||
std::clamp(p.x() - _brushMargins.left(), 0., _contentRect.x()),
|
||||
std::clamp(p.y() - _brushMargins.top(), 0., _contentRect.y())),
|
||||
QPointF(
|
||||
std::clamp(
|
||||
p.x() + _brushMargins.right(),
|
||||
_contentRect.x() + _contentRect.width(),
|
||||
sceneSize.width()),
|
||||
std::clamp(
|
||||
p.y() + _brushMargins.bottom(),
|
||||
_contentRect.y() + _contentRect.height(),
|
||||
sceneSize.height())));
|
||||
}
|
||||
|
||||
std::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const {
|
||||
if (points.size() < 4) {
|
||||
return points;
|
||||
}
|
||||
auto result = std::vector<StrokePoint>();
|
||||
result.reserve(points.size());
|
||||
result.push_back(points[0]);
|
||||
result.push_back(points[1]);
|
||||
for (auto i = 2; i < int(points.size()) - 1; ++i) {
|
||||
const auto &prev = points[i - 1].pos;
|
||||
const auto &curr = points[i].pos;
|
||||
const auto &next = points[i + 1].pos;
|
||||
const auto smoothed = curr * kInvStrength
|
||||
+ (prev + next) * kHalfStrength;
|
||||
result.push_back({
|
||||
.pos = smoothed,
|
||||
.pressure = points[i].pressure,
|
||||
.time = points[i].time,
|
||||
});
|
||||
}
|
||||
result.push_back(points.back());
|
||||
return result;
|
||||
}
|
||||
|
||||
void ItemCanvas::renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx) {
|
||||
if (points.size() < 2 || startIdx >= int(points.size()) - 1) {
|
||||
return;
|
||||
}
|
||||
auto path = QPainterPath();
|
||||
const auto effectiveStart = std::max(0, startIdx);
|
||||
path.moveTo(points[effectiveStart].pos);
|
||||
for (auto i = effectiveStart; i < int(points.size()) - 1; ++i) {
|
||||
const auto &p0 = points[i].pos;
|
||||
const auto &p1 = points[i + 1].pos;
|
||||
const auto ctrl = (p0 + p1) / 2.0;
|
||||
if (i == effectiveStart) {
|
||||
path.lineTo(ctrl);
|
||||
} else {
|
||||
path.quadTo(p0, ctrl);
|
||||
}
|
||||
}
|
||||
path.lineTo(points.back().pos);
|
||||
const auto count = points.size() - std::max(0, startIdx);
|
||||
const auto avgPressure = count > 0
|
||||
? std::accumulate(
|
||||
points.begin() + std::max(0, startIdx),
|
||||
points.end(),
|
||||
0.0,
|
||||
[](float64 sum, const StrokePoint &p) {
|
||||
return sum + p.pressure;
|
||||
}) / count
|
||||
: 1.0;
|
||||
const auto width = _brushData.size * avgPressure;
|
||||
auto stroker = QPainterPathStroker();
|
||||
stroker.setWidth(width);
|
||||
stroker.setCapStyle(Qt::RoundCap);
|
||||
stroker.setJoinStyle(Qt::RoundJoin);
|
||||
const auto outline = stroker.createStroke(path);
|
||||
_p->fillPath(outline, _brushData.color);
|
||||
_rectToUpdate |= outline.boundingRect() + _brushMargins;
|
||||
}
|
||||
|
||||
void ItemCanvas::drawIncrementalStroke() {
|
||||
if (_currentStroke.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto startIdx = std::max(
|
||||
0,
|
||||
_lastRenderedIndex - kSegmentOverlap);
|
||||
auto segment = std::vector<StrokePoint>(
|
||||
_currentStroke.begin() + startIdx,
|
||||
_currentStroke.end());
|
||||
if (segment.size() < 2) {
|
||||
return;
|
||||
}
|
||||
if (segment.size() >= 4) {
|
||||
for (auto i = 0; i < 2; ++i) {
|
||||
segment = smoothStroke(segment);
|
||||
}
|
||||
}
|
||||
renderSegment(
|
||||
segment,
|
||||
std::min(kSegmentOverlap, int(segment.size()) - 1));
|
||||
_lastRenderedIndex = std::max(
|
||||
0,
|
||||
int(_currentStroke.size()) - kSegmentOverlap);
|
||||
}
|
||||
|
||||
void ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {
|
||||
if (!_currentStroke.empty()) {
|
||||
const auto distance = PointDistance(
|
||||
point,
|
||||
_currentStroke.back().pos);
|
||||
const auto minDistance = kMinPointDistanceBase * std::min(1.0, _zoom);
|
||||
if (distance < minDistance) {
|
||||
return;
|
||||
}
|
||||
if (distance > kMaxPointDistance) {
|
||||
const auto steps = int(std::ceil(distance / kMaxPointDistance));
|
||||
const auto &lastPos = _currentStroke.back().pos;
|
||||
const auto &lastPressure = _currentStroke.back().pressure;
|
||||
for (auto i = 1; i < steps; ++i) {
|
||||
const auto t = float64(i) / steps;
|
||||
const auto interpolated = lastPos * (1.0 - t) + point * t;
|
||||
const auto interpTime = _lastPointTime
|
||||
+ int64((time - _lastPointTime) * t);
|
||||
_currentStroke.push_back({
|
||||
.pos = interpolated,
|
||||
.pressure = lastPressure,
|
||||
.time = interpTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto timeDelta = _lastPointTime
|
||||
? std::max(int64(1), time - _lastPointTime)
|
||||
: kBatchUpdateInterval;
|
||||
const auto speed = !_currentStroke.empty()
|
||||
? PointDistance(point, _currentStroke.back().pos) / timeDelta
|
||||
: 0.0;
|
||||
const auto pressureFromSpeed = std::clamp(
|
||||
1.0 - speed * 0.1,
|
||||
kMinPressure,
|
||||
1.0);
|
||||
const auto pressure = _currentStroke.empty()
|
||||
? 1.0
|
||||
: _currentStroke.back().pressure * kPressureDecay
|
||||
+ pressureFromSpeed * (1.0 - kPressureDecay);
|
||||
_currentStroke.push_back({
|
||||
.pos = point,
|
||||
.pressure = pressure,
|
||||
.time = time,
|
||||
});
|
||||
_lastPointTime = time;
|
||||
computeContentRect(point);
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMousePressEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
_lastPoint = e->scenePos();
|
||||
_rectToUpdate = QRectF();
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(_lastPoint, now);
|
||||
_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;
|
||||
_drawing = true;
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMouseMoveEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
if (!_drawing) {
|
||||
return;
|
||||
}
|
||||
const auto scenePos = e->scenePos();
|
||||
const auto now = crl::now();
|
||||
addStrokePoint(scenePos, now);
|
||||
_lastPoint = scenePos;
|
||||
if (_currentStroke.size() - _lastRenderedIndex >= 3) {
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemCanvas::handleMouseReleaseEvent(
|
||||
not_null<QGraphicsSceneMouseEvent*> e) {
|
||||
if (!_drawing) {
|
||||
return;
|
||||
}
|
||||
_drawing = false;
|
||||
drawIncrementalStroke();
|
||||
update(_rectToUpdate);
|
||||
if (_contentRect.isValid()) {
|
||||
const auto scaledContentRect = QRectF(
|
||||
_contentRect.x() * style::DevicePixelRatio(),
|
||||
_contentRect.y() * style::DevicePixelRatio(),
|
||||
_contentRect.width() * style::DevicePixelRatio(),
|
||||
_contentRect.height() * style::DevicePixelRatio());
|
||||
_grabContentRequests.fire({
|
||||
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
|
||||
.position = _contentRect.topLeft(),
|
||||
});
|
||||
}
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemCanvas::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *,
|
||||
QWidget *) {
|
||||
p->fillRect(_rectToUpdate, Qt::transparent);
|
||||
p->drawPixmap(0, 0, _pixmap);
|
||||
_rectToUpdate = QRectF();
|
||||
}
|
||||
|
||||
rpl::producer<ItemCanvas::Content> ItemCanvas::grabContentRequests() const {
|
||||
return _grabContentRequests.events();
|
||||
}
|
||||
|
||||
bool ItemCanvas::collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemCanvas::collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ItemCanvas::cancelDrawing() {
|
||||
_drawing = false;
|
||||
_currentStroke.clear();
|
||||
_lastRenderedIndex = 0;
|
||||
_lastPointTime = 0;
|
||||
_currentPath = QPainterPath();
|
||||
_contentRect = QRectF();
|
||||
clearPixmap();
|
||||
update();
|
||||
}
|
||||
|
||||
void ItemCanvas::updateZoom(float64 zoom) {
|
||||
_zoom = zoom;
|
||||
}
|
||||
|
||||
ItemCanvas::~ItemCanvas() {
|
||||
_hq = nullptr;
|
||||
_p = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
96
Telegram/SourceFiles/editor/scene/scene_item_canvas.h
Normal file
96
Telegram/SourceFiles/editor/scene/scene_item_canvas.h
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
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 "ui/painter.h"
|
||||
|
||||
#include <QGraphicsItem>
|
||||
|
||||
class QGraphicsSceneMouseEvent;
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemCanvas : public QGraphicsItem {
|
||||
public:
|
||||
struct Content {
|
||||
QPixmap pixmap;
|
||||
QPointF position;
|
||||
};
|
||||
|
||||
ItemCanvas();
|
||||
~ItemCanvas();
|
||||
|
||||
void applyBrush(const QColor &color, float size);
|
||||
void clearPixmap();
|
||||
void cancelDrawing();
|
||||
void updateZoom(float64 zoom);
|
||||
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
|
||||
void handleMousePressEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
void handleMouseReleaseEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
void handleMouseMoveEvent(not_null<QGraphicsSceneMouseEvent*> event);
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> grabContentRequests() const;
|
||||
|
||||
protected:
|
||||
bool collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
|
||||
bool collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
private:
|
||||
struct StrokePoint {
|
||||
QPointF pos;
|
||||
float64 pressure = 1.0;
|
||||
int64 time = 0;
|
||||
};
|
||||
|
||||
void computeContentRect(const QPointF &p);
|
||||
void addStrokePoint(const QPointF &point, int64 time);
|
||||
void drawIncrementalStroke();
|
||||
std::vector<StrokePoint> smoothStroke(
|
||||
const std::vector<StrokePoint> &points) const;
|
||||
void renderSegment(
|
||||
const std::vector<StrokePoint> &points,
|
||||
int startIdx);
|
||||
|
||||
bool _drawing = false;
|
||||
std::vector<StrokePoint> _currentStroke;
|
||||
int _lastRenderedIndex = 0;
|
||||
float64 _zoom = 1.0;
|
||||
int64 _lastPointTime = 0;
|
||||
|
||||
std::unique_ptr<PainterHighQualityEnabler> _hq;
|
||||
std::unique_ptr<Painter> _p;
|
||||
|
||||
QRectF _rectToUpdate;
|
||||
QRectF _contentRect;
|
||||
QMarginsF _brushMargins;
|
||||
|
||||
QPointF _lastPoint;
|
||||
|
||||
QPixmap _pixmap;
|
||||
QPainterPath _currentPath;
|
||||
|
||||
struct {
|
||||
float size = 1.;
|
||||
QColor color;
|
||||
} _brushData;
|
||||
|
||||
rpl::event_stream<Content> _grabContentRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
46
Telegram/SourceFiles/editor/scene/scene_item_image.cpp
Normal file
46
Telegram/SourceFiles/editor/scene/scene_item_image.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
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_image.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
ItemImage::ItemImage(
|
||||
QPixmap &&pixmap,
|
||||
ItemBase::Data data)
|
||||
: ItemBase(std::move(data))
|
||||
, _pixmap(std::move(pixmap)) {
|
||||
setAspectRatio(_pixmap.isNull()
|
||||
? 1.0
|
||||
: (_pixmap.height() / float64(_pixmap.width())));
|
||||
}
|
||||
|
||||
void ItemImage::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *w) {
|
||||
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.);
|
||||
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||
ItemBase::paint(p, option, w);
|
||||
}
|
||||
|
||||
void ItemImage::performFlip() {
|
||||
_pixmap = _pixmap.transformed(QTransform().scale(-1, 1));
|
||||
update();
|
||||
}
|
||||
|
||||
std::shared_ptr<ItemBase> ItemImage::duplicate(ItemBase::Data data) const {
|
||||
auto pixmap = _pixmap;
|
||||
return std::make_shared<ItemImage>(std::move(pixmap), std::move(data));
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
31
Telegram/SourceFiles/editor/scene/scene_item_image.h
Normal file
31
Telegram/SourceFiles/editor/scene/scene_item_image.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 "editor/scene/scene_item_base.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemImage : public ItemBase {
|
||||
public:
|
||||
ItemImage(
|
||||
QPixmap &&pixmap,
|
||||
ItemBase::Data data);
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
protected:
|
||||
void performFlip() override;
|
||||
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||
private:
|
||||
QPixmap _pixmap;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
62
Telegram/SourceFiles/editor/scene/scene_item_line.cpp
Normal file
62
Telegram/SourceFiles/editor/scene/scene_item_line.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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_line.h"
|
||||
|
||||
#include <QGraphicsScene>
|
||||
|
||||
namespace Editor {
|
||||
|
||||
ItemLine::ItemLine(QPixmap &&pixmap)
|
||||
: _pixmap(std::move(pixmap))
|
||||
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {
|
||||
}
|
||||
|
||||
QRectF ItemLine::boundingRect() const {
|
||||
return _rect;
|
||||
}
|
||||
|
||||
void ItemLine::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *,
|
||||
QWidget *) {
|
||||
p->drawPixmap(0, 0, _pixmap);
|
||||
}
|
||||
|
||||
bool ItemLine::collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
bool ItemLine::collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ItemLine::save(SaveState state) {
|
||||
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
saved = {
|
||||
.saved = true,
|
||||
.status = status(),
|
||||
};
|
||||
}
|
||||
|
||||
void ItemLine::restore(SaveState state) {
|
||||
if (!hasState(state)) {
|
||||
return;
|
||||
}
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
setStatus(saved.status);
|
||||
}
|
||||
|
||||
bool ItemLine::hasState(SaveState state) const {
|
||||
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
|
||||
return saved.saved;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
44
Telegram/SourceFiles/editor/scene/scene_item_line.h
Normal file
44
Telegram/SourceFiles/editor/scene/scene_item_line.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 "editor/scene/scene_item_base.h"
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemLine : public NumberedItem {
|
||||
public:
|
||||
ItemLine(QPixmap &&pixmap);
|
||||
QRectF boundingRect() const override;
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
|
||||
bool hasState(SaveState state) const override;
|
||||
void save(SaveState state) override;
|
||||
void restore(SaveState state) override;
|
||||
protected:
|
||||
bool collidesWithItem(
|
||||
const QGraphicsItem *,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
bool collidesWithPath(
|
||||
const QPainterPath &,
|
||||
Qt::ItemSelectionMode) const override;
|
||||
private:
|
||||
const QPixmap _pixmap;
|
||||
const QRectF _rect;
|
||||
|
||||
struct {
|
||||
bool saved = false;
|
||||
NumberedItem::Status status;
|
||||
} _saved, _keeped;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
140
Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp
Normal file
140
Telegram/SourceFiles/editor/scene/scene_item_sticker.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
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_sticker.h"
|
||||
|
||||
#include "chat_helpers/stickers_lottie.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "lottie/lottie_common.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_editor.h"
|
||||
|
||||
namespace Editor {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSticker::ItemSticker(
|
||||
not_null<DocumentData*> document,
|
||||
ItemBase::Data data)
|
||||
: ItemBase(std::move(data))
|
||||
, _document(document)
|
||||
, _mediaView(_document->createMediaView()) {
|
||||
const auto stickerData = document->sticker();
|
||||
if (!stickerData) {
|
||||
return;
|
||||
}
|
||||
const auto updateThumbnail = [=] {
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (_image.isNull()) {
|
||||
setAspectRatio(1.);
|
||||
}
|
||||
});
|
||||
if (stickerData->isLottie()) {
|
||||
_lottie.player = ChatHelpers::LottiePlayerFromDocument(
|
||||
_mediaView.get(),
|
||||
ChatHelpers::StickerLottieSize::MessageHistory,
|
||||
QSize(kStickerSideSize, kStickerSideSize)
|
||||
* style::DevicePixelRatio(),
|
||||
Lottie::Quality::High);
|
||||
_lottie.player->updates(
|
||||
) | rpl::on_next([=] {
|
||||
updatePixmap(_lottie.player->frame());
|
||||
_lottie.player = nullptr;
|
||||
_lottie.lifetime.destroy();
|
||||
update();
|
||||
}, _lottie.lifetime);
|
||||
return true;
|
||||
} else if (stickerData->isWebm()
|
||||
&& !_document->dimensions.isEmpty()) {
|
||||
const auto callback = [=](::Media::Clip::Notification) {
|
||||
const auto size = _document->dimensions;
|
||||
if (_webm && _webm->ready() && !_webm->started()) {
|
||||
_webm->start({ .frame = size, .keepAlpha = true });
|
||||
}
|
||||
if (_webm && _webm->started()) {
|
||||
updatePixmap(_webm->current(
|
||||
{ .frame = size, .keepAlpha = true },
|
||||
0));
|
||||
_webm = nullptr;
|
||||
}
|
||||
};
|
||||
_webm = ::Media::Clip::MakeReader(
|
||||
_mediaView->owner()->location(),
|
||||
_mediaView->bytes(),
|
||||
callback);
|
||||
return true;
|
||||
}
|
||||
const auto sticker = _mediaView->getStickerLarge();
|
||||
if (!sticker) {
|
||||
return false;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
auto pixmap = sticker->pixNoCache(sticker->size() * ratio);
|
||||
pixmap.setDevicePixelRatio(ratio);
|
||||
updatePixmap(pixmap.toImage());
|
||||
return true;
|
||||
};
|
||||
if (!updateThumbnail()) {
|
||||
_document->session().downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
if (updateThumbnail()) {
|
||||
_loadingLifetime.destroy();
|
||||
update();
|
||||
}
|
||||
}, _loadingLifetime);
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSticker::updatePixmap(QImage &&image) {
|
||||
_image = std::move(image);
|
||||
if (flipped()) {
|
||||
performFlip();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
if (!_image.isNull()) {
|
||||
setAspectRatio(_image.height() / float64(_image.width()));
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSticker::paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *w) {
|
||||
const auto rect = contentRect();
|
||||
const auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio())
|
||||
.scaled(rect.size(), Qt::KeepAspectRatio);
|
||||
const auto resultRect = QRectF(rect.topLeft(), imageSize).translated(
|
||||
(rect.width() - imageSize.width()) / 2.,
|
||||
(rect.height() - imageSize.height()) / 2.);
|
||||
p->drawImage(resultRect, _image);
|
||||
ItemBase::paint(p, option, w);
|
||||
}
|
||||
|
||||
not_null<DocumentData*> ItemSticker::sticker() const {
|
||||
return _document;
|
||||
}
|
||||
|
||||
int ItemSticker::type() const {
|
||||
return Type;
|
||||
}
|
||||
|
||||
void ItemSticker::performFlip() {
|
||||
_image = _image.transformed(QTransform().scale(-1, 1));
|
||||
update();
|
||||
}
|
||||
|
||||
std::shared_ptr<ItemBase> ItemSticker::duplicate(ItemBase::Data data) const {
|
||||
return std::make_shared<ItemSticker>(_document, std::move(data));
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
58
Telegram/SourceFiles/editor/scene/scene_item_sticker.h
Normal file
58
Telegram/SourceFiles/editor/scene/scene_item_sticker.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 "editor/scene/scene_item_base.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
namespace Lottie {
|
||||
class SinglePlayer;
|
||||
} // namespace Lottie
|
||||
class DocumentData;
|
||||
|
||||
namespace Editor {
|
||||
|
||||
class ItemSticker : public ItemBase {
|
||||
public:
|
||||
enum { Type = ItemBase::Type + 1 };
|
||||
|
||||
ItemSticker(
|
||||
not_null<DocumentData*> document,
|
||||
ItemBase::Data data);
|
||||
void paint(
|
||||
QPainter *p,
|
||||
const QStyleOptionGraphicsItem *option,
|
||||
QWidget *widget) override;
|
||||
[[nodiscard]] not_null<DocumentData*> sticker() const;
|
||||
int type() const override;
|
||||
|
||||
protected:
|
||||
void performFlip() override;
|
||||
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||
|
||||
private:
|
||||
const not_null<DocumentData*> _document;
|
||||
const std::shared_ptr<::Data::DocumentMedia> _mediaView;
|
||||
|
||||
void updatePixmap(QImage &&image);
|
||||
|
||||
struct {
|
||||
std::unique_ptr<Lottie::SinglePlayer> player;
|
||||
rpl::lifetime lifetime;
|
||||
} _lottie;
|
||||
::Media::Clip::ReaderPointer _webm;
|
||||
QImage _image;
|
||||
|
||||
rpl::lifetime _loadingLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
Reference in New Issue
Block a user