mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
884 lines
22 KiB
C++
884 lines
22 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_animated.h"
|
|
#include "editor/scene/scene_item_canvas.h"
|
|
#include "editor/scene/scene_item_line.h"
|
|
#include "editor/scene/scene_item_shape.h"
|
|
#include "editor/scene/scene_item_text.h"
|
|
#include "editor/scene/scene_text_editing.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rp_widget.h"
|
|
#include "styles/style_editor.h"
|
|
|
|
#include <QGraphicsSceneMouseEvent>
|
|
#include <QtMath>
|
|
|
|
namespace Editor {
|
|
namespace {
|
|
|
|
using ItemPtr = std::shared_ptr<NumberedItem>;
|
|
|
|
class ItemAction : public NumberedItem {
|
|
public:
|
|
using NumberedItem::NumberedItem;
|
|
|
|
virtual void apply() = 0;
|
|
virtual void revert() = 0;
|
|
|
|
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);
|
|
}
|
|
|
|
private:
|
|
struct {
|
|
bool saved = false;
|
|
NumberedItem::Status status = Status::Normal;
|
|
} _saved, _keeped;
|
|
};
|
|
|
|
class ItemEraser final : public ItemAction {
|
|
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() override {
|
|
for (const auto &target : _targets) {
|
|
target.item->applyEraser(_mask, _maskPos);
|
|
}
|
|
}
|
|
|
|
void revert() override {
|
|
for (const auto &target : _targets) {
|
|
target.item->setPixmap(target.before);
|
|
}
|
|
}
|
|
|
|
void restore(SaveState state) override {
|
|
if (!hasState(state)) {
|
|
return;
|
|
}
|
|
ItemAction::restore(state);
|
|
if (isNormalStatus()) {
|
|
apply();
|
|
} else if (isUndidStatus()) {
|
|
revert();
|
|
}
|
|
}
|
|
|
|
private:
|
|
QPixmap _mask;
|
|
QPointF _maskPos;
|
|
std::vector<Target> _targets;
|
|
};
|
|
|
|
class ItemPlacement final : public ItemAction {
|
|
public:
|
|
struct Target {
|
|
std::shared_ptr<ItemBase> item;
|
|
ItemBase::Placement before;
|
|
ItemBase::Placement after;
|
|
};
|
|
|
|
explicit ItemPlacement(std::vector<Target> targets)
|
|
: _targets(std::move(targets)) {
|
|
}
|
|
|
|
void apply() override {
|
|
for (const auto &target : _targets) {
|
|
target.item->applyPlacement(target.after);
|
|
}
|
|
}
|
|
|
|
void revert() override {
|
|
for (const auto &target : _targets) {
|
|
target.item->applyPlacement(target.before);
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::vector<Target> _targets;
|
|
};
|
|
|
|
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
|
|
return event->isAccepted() || (event->button() == Qt::RightButton);
|
|
}
|
|
|
|
constexpr auto kShapeDragThreshold = 4.;
|
|
constexpr auto kShapeSnapAngle = 45.;
|
|
constexpr auto kDraftShapeOpacity = 0.5;
|
|
|
|
} // namespace
|
|
|
|
Scene::Scene(const QRectF &rect)
|
|
: QGraphicsScene(rect)
|
|
, _canvas(std::make_shared<ItemCanvas>())
|
|
, _lastZ(std::make_shared<float64>(9000.))
|
|
, _textEdit(std::make_unique<TextEditController>(this)) {
|
|
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);
|
|
auto *shapeItem = (ItemShape*)(nullptr);
|
|
if (selected.size() == 1) {
|
|
if (selected.front()->type() == ItemText::Type) {
|
|
textItem = static_cast<ItemText*>(selected.front());
|
|
} else if (selected.front()->type() == ItemShape::Type) {
|
|
shapeItem = static_cast<ItemShape*>(selected.front());
|
|
}
|
|
}
|
|
if (textItem != _selectedTextItem) {
|
|
_selectedTextItem = textItem;
|
|
if (textItem) {
|
|
_textItemSelections.fire_copy(textItem->color());
|
|
} else {
|
|
_textItemDeselections.fire({});
|
|
}
|
|
}
|
|
if (shapeItem != _selectedShapeItem) {
|
|
_selectedShapeItem = shapeItem;
|
|
if (shapeItem) {
|
|
_shapeItemSelections.fire_copy(shapeItem->color());
|
|
} else {
|
|
_shapeItemDeselections.fire({});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void Scene::cancelDrawing() {
|
|
_textEdit->finishEditing(false);
|
|
_canvas->cancelDrawing();
|
|
}
|
|
|
|
void Scene::cancelTextEditing() {
|
|
_textEdit->finishEditing(false, false);
|
|
}
|
|
|
|
void Scene::addItem(ItemPtr item) {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
item->setNumber(_itemNumber++);
|
|
if (item->scene() != this) {
|
|
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 (_shapeTool.pending) {
|
|
if (event->button() == Qt::LeftButton) {
|
|
event->accept();
|
|
startShapeDrawing(event->scenePos());
|
|
return;
|
|
} else if (event->button() == Qt::RightButton) {
|
|
event->accept();
|
|
if (_shapeTool.dragging) {
|
|
finishShapeDrawing(false);
|
|
} else {
|
|
setPendingShape(std::nullopt);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (_textEdit->editing()
|
|
&& !_textEdit->proxyContains(event->scenePos())) {
|
|
_textEdit->finishEditing(true);
|
|
QGraphicsScene::mousePressEvent(event);
|
|
capturePlacements();
|
|
return;
|
|
}
|
|
|
|
QGraphicsScene::mousePressEvent(event);
|
|
capturePlacements();
|
|
if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {
|
|
return;
|
|
}
|
|
_canvas->handleMousePressEvent(event);
|
|
}
|
|
|
|
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
|
if (_shapeTool.dragging && (event->button() == Qt::LeftButton)) {
|
|
event->accept();
|
|
finishShapeDrawing(true);
|
|
return;
|
|
}
|
|
QGraphicsScene::mouseReleaseEvent(event);
|
|
commitPlacements();
|
|
if (SkipMouseEvent(event) || _textEdit->editing()) {
|
|
return;
|
|
}
|
|
_canvas->handleMouseReleaseEvent(event);
|
|
}
|
|
|
|
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
|
if (_shapeTool.dragging) {
|
|
event->accept();
|
|
updateShapeDrawing(event->scenePos(), event->modifiers());
|
|
return;
|
|
}
|
|
QGraphicsScene::mouseMoveEvent(event);
|
|
if (SkipMouseEvent(event) || _textEdit->editing()) {
|
|
return;
|
|
}
|
|
_canvas->handleMouseMoveEvent(event);
|
|
}
|
|
|
|
void Scene::setPendingShape(std::optional<PendingShape> pending) {
|
|
if (!pending && !_shapeTool.pending) {
|
|
return;
|
|
}
|
|
if (_shapeTool.dragging) {
|
|
finishShapeDrawing(false);
|
|
}
|
|
const auto was = _shapeTool.pending.has_value();
|
|
_shapeTool.pending = std::move(pending);
|
|
const auto now = _shapeTool.pending.has_value();
|
|
if (now) {
|
|
_textEdit->finishEditing(true);
|
|
clearSelection();
|
|
clearFocus();
|
|
}
|
|
if (was != now) {
|
|
_pendingShapeStates.fire_copy(now);
|
|
}
|
|
}
|
|
|
|
void Scene::updatePendingShapeBrush(
|
|
const QColor &color,
|
|
float64 strokeWidth) {
|
|
if (!_shapeTool.pending) {
|
|
return;
|
|
}
|
|
_shapeTool.pending->color = color;
|
|
_shapeTool.pending->strokeWidth = strokeWidth;
|
|
if (const auto item = _shapeTool.item.get()) {
|
|
item->setColor(color);
|
|
item->setStrokeWidth(strokeWidth);
|
|
}
|
|
}
|
|
|
|
bool Scene::hasPendingShape() const {
|
|
return _shapeTool.pending.has_value();
|
|
}
|
|
|
|
rpl::producer<bool> Scene::pendingShapeStates() const {
|
|
return _pendingShapeStates.events();
|
|
}
|
|
|
|
std::shared_ptr<ItemShape> Scene::createShape(
|
|
int size,
|
|
const QPointF ¢er) const {
|
|
const auto &pending = *_shapeTool.pending;
|
|
auto data = ItemBase::Data{
|
|
.initialZoom = (_currentZoom > 0.) ? _currentZoom : 1.,
|
|
.zPtr = _lastZ,
|
|
.size = size,
|
|
.x = int(center.x()),
|
|
.y = int(center.y()),
|
|
.flipped = pending.flipped,
|
|
.rotation = pending.rotation,
|
|
.imageSize = sceneRect().size().toSize(),
|
|
};
|
|
return std::make_shared<ItemShape>(
|
|
pending.shape,
|
|
pending.color,
|
|
pending.strokeWidth,
|
|
pending.fill,
|
|
std::move(data));
|
|
}
|
|
|
|
void Scene::startShapeDrawing(const QPointF &position) {
|
|
if (_shapeTool.item) {
|
|
finishShapeDrawing(false);
|
|
}
|
|
clearSelection();
|
|
cancelDrawing();
|
|
|
|
_shapeTool.start = position;
|
|
_shapeTool.dragging = true;
|
|
_shapeTool.moved = false;
|
|
_shapeTool.fits = false;
|
|
_shapeTool.item = createShape(0, position);
|
|
_shapeTool.item->setVisible(false);
|
|
QGraphicsScene::addItem(_shapeTool.item.get());
|
|
}
|
|
|
|
void Scene::updateShapeDrawing(
|
|
const QPointF &position,
|
|
Qt::KeyboardModifiers modifiers) {
|
|
const auto item = _shapeTool.item.get();
|
|
if (!item || !_shapeTool.pending) {
|
|
return;
|
|
}
|
|
const auto delta = position - _shapeTool.start;
|
|
const auto length = std::hypot(delta.x(), delta.y());
|
|
if (!_shapeTool.moved) {
|
|
const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.;
|
|
if (length * zoom < kShapeDragThreshold) {
|
|
return;
|
|
}
|
|
_shapeTool.moved = true;
|
|
item->setVisible(true);
|
|
item->setSelected(true);
|
|
}
|
|
const auto &pending = *_shapeTool.pending;
|
|
const auto radians = pending.rotation * M_PI / 180.;
|
|
const auto cosine = std::cos(radians);
|
|
const auto sine = std::sin(radians);
|
|
auto width = delta.x() * cosine + delta.y() * sine;
|
|
auto height = delta.y() * cosine - delta.x() * sine;
|
|
const auto shift = modifiers.testFlag(Qt::ShiftModifier);
|
|
if (pending.shape == ShapeType::Arrow) {
|
|
const auto mirror = pending.flipped ? -1. : 1.;
|
|
const auto degrees = mirror
|
|
* std::atan2(height, mirror * width)
|
|
* 180.
|
|
/ M_PI;
|
|
item->setRotation(pending.rotation + (shift
|
|
? (base::SafeRound(degrees / kShapeSnapAngle) * kShapeSnapAngle)
|
|
: degrees));
|
|
item->setPos(_shapeTool.start + delta / 2.);
|
|
applyDraftFrame(length, 0.);
|
|
return;
|
|
}
|
|
if (shift) {
|
|
const auto aspectRatio = item->defaultAspectRatio();
|
|
const auto side = std::max(
|
|
std::abs(width),
|
|
std::abs(height) / aspectRatio);
|
|
width = (width < 0.) ? -side : side;
|
|
height = ((height < 0.) ? -side : side) * aspectRatio;
|
|
}
|
|
item->setPos(_shapeTool.start
|
|
+ QPointF(cosine, sine) * (width / 2.)
|
|
+ QPointF(-sine, cosine) * (height / 2.));
|
|
applyDraftFrame(std::abs(width), std::abs(height));
|
|
}
|
|
|
|
void Scene::applyDraftFrame(float64 width, float64 height) {
|
|
const auto item = _shapeTool.item.get();
|
|
_shapeTool.fits = item->applyDraftFrame(width, height);
|
|
item->setOpacity(_shapeTool.fits ? 1. : kDraftShapeOpacity);
|
|
}
|
|
|
|
void Scene::finishShapeDrawing(bool apply) {
|
|
auto item = base::take(_shapeTool.item);
|
|
const auto moved = base::take(_shapeTool.moved);
|
|
const auto fits = base::take(_shapeTool.fits);
|
|
_shapeTool.dragging = false;
|
|
if (!item) {
|
|
return;
|
|
}
|
|
if (!apply || (moved && !fits)) {
|
|
item->setSelected(false);
|
|
QGraphicsScene::removeItem(item.get());
|
|
return;
|
|
}
|
|
if (!moved) {
|
|
const auto size = _shapeTool.pending
|
|
? _shapeTool.pending->defaultSize
|
|
: 0;
|
|
item->applyFrame(size, size * item->defaultAspectRatio());
|
|
item->setVisible(true);
|
|
item->setSelected(true);
|
|
}
|
|
addItem(item);
|
|
item->setFocus();
|
|
setPendingShape(std::nullopt);
|
|
}
|
|
|
|
void Scene::applyBrush(const QColor &color, float64 size, Brush::Tool tool) {
|
|
_canvas->applyBrush(color, size, tool);
|
|
}
|
|
|
|
void Scene::setTextDefaults(
|
|
const QColor &color,
|
|
float64 fontSize,
|
|
TextStyle style,
|
|
TextTypeface typeface,
|
|
TextAlignment alignment) {
|
|
_textEdit->setDefaults(color, fontSize, style, typeface, alignment);
|
|
}
|
|
|
|
void Scene::applyTextPrefs(const TextPrefs &prefs) {
|
|
_textEdit->applyPrefs(prefs);
|
|
}
|
|
|
|
void Scene::noteTextItemPrefs(not_null<ItemText*> item) {
|
|
_textEdit->noteItemPrefs(item);
|
|
}
|
|
|
|
void Scene::setTextColor(const QColor &color) {
|
|
_textEdit->setColor(color);
|
|
}
|
|
|
|
void Scene::setSelectedTextColor(const QColor &color) {
|
|
for (auto *item : selectedItems()) {
|
|
if (item->type() == ItemText::Type) {
|
|
static_cast<ItemText*>(item)->setColor(color);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Scene::setSelectedShapeBrush(
|
|
const QColor &color,
|
|
float64 strokeWidth) {
|
|
for (auto *item : selectedItems()) {
|
|
if (item->type() == ItemShape::Type) {
|
|
const auto shape = static_cast<ItemShape*>(item);
|
|
shape->setColor(color);
|
|
shape->setStrokeWidth(strokeWidth);
|
|
}
|
|
}
|
|
}
|
|
|
|
rpl::producer<QColor> Scene::textColorRequests() const {
|
|
return _textEdit->colorRequests();
|
|
}
|
|
|
|
rpl::producer<TextPrefs> Scene::textPrefsUsed() const {
|
|
return _textEdit->prefsUsed();
|
|
}
|
|
|
|
rpl::producer<QColor> Scene::textItemSelections() const {
|
|
return _textItemSelections.events();
|
|
}
|
|
|
|
rpl::producer<> Scene::textItemDeselections() const {
|
|
return _textItemDeselections.events();
|
|
}
|
|
|
|
rpl::producer<bool> Scene::textEditStates() const {
|
|
return _textEdit->editStates();
|
|
}
|
|
|
|
rpl::producer<QColor> Scene::shapeItemSelections() const {
|
|
return _shapeItemSelections.events();
|
|
}
|
|
|
|
rpl::producer<> Scene::shapeItemDeselections() const {
|
|
return _shapeItemDeselections.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;
|
|
}
|
|
|
|
bool Scene::hasAnimatedItems() const {
|
|
for (const auto &item : _items) {
|
|
const auto animated = item->isNormalStatus()
|
|
? dynamic_cast<ItemAnimated*>(item.get())
|
|
: nullptr;
|
|
if (animated
|
|
&& animated->animated()
|
|
&& !animated->content().isEmpty()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Scene::releaseAnimations() {
|
|
for (const auto &item : _items) {
|
|
if (const auto animated = dynamic_cast<ItemAnimated*>(item.get())) {
|
|
animated->releasePlayers();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<float64> Scene::lastZ() const {
|
|
return _lastZ;
|
|
}
|
|
|
|
Scene::ItemPtr Scene::itemShared(QGraphicsItem *item) const {
|
|
const auto it = _itemsByPointer.find(item);
|
|
return (it != end(_itemsByPointer)) ? it->second : nullptr;
|
|
}
|
|
|
|
float64 Scene::currentZoom() const {
|
|
return _currentZoom;
|
|
}
|
|
|
|
void Scene::updateZoom(float64 zoom) {
|
|
_currentZoom = zoom;
|
|
_canvas->updateZoom(zoom);
|
|
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, [](const ItemPtr &item) {
|
|
return item->isNormalStatus() && item->undoable();
|
|
});
|
|
}
|
|
|
|
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, [](const ItemPtr &item) {
|
|
return item->isNormalStatus() && item->undoable();
|
|
});
|
|
if (it != filtered.end()) {
|
|
if (const auto action = dynamic_cast<ItemAction*>(it->get())) {
|
|
action->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 action = dynamic_cast<ItemAction*>(it->get())) {
|
|
action->apply();
|
|
}
|
|
(*it)->setStatus(NumberedItem::Status::Normal);
|
|
}
|
|
}
|
|
|
|
void Scene::capturePlacements() {
|
|
_capturedPlacements.clear();
|
|
for (const auto &item : _items) {
|
|
if (item->isNormalStatus() && (item->type() >= ItemBase::Type)) {
|
|
const auto base = std::static_pointer_cast<ItemBase>(item);
|
|
_capturedPlacements.push_back({
|
|
.item = base,
|
|
.placement = base->placement(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void Scene::commitPlacements() {
|
|
if (_capturedPlacements.empty()) {
|
|
return;
|
|
}
|
|
auto targets = std::vector<ItemPlacement::Target>();
|
|
for (auto &captured : _capturedPlacements) {
|
|
const auto now = captured.item->placement();
|
|
if (now != captured.placement) {
|
|
targets.push_back({
|
|
.item = std::move(captured.item),
|
|
.before = captured.placement,
|
|
.after = now,
|
|
});
|
|
}
|
|
}
|
|
_capturedPlacements.clear();
|
|
if (!targets.empty()) {
|
|
addItem(std::make_shared<ItemPlacement>(std::move(targets)));
|
|
}
|
|
}
|
|
|
|
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) {
|
|
_textEdit->finishEditing(true);
|
|
for (const auto &item : _items) {
|
|
if (item->isNormalStatus()
|
|
&& (item->type() == ItemText::Type)) {
|
|
static_cast<ItemText*>(item.get())->bakeScale();
|
|
}
|
|
}
|
|
|
|
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::createTextAtCenter(int rotation, bool flipped) {
|
|
_textEdit->createAtCenter(rotation, flipped);
|
|
}
|
|
|
|
void Scene::startTextEditing(ItemText *item) {
|
|
_textEdit->startEditing(item);
|
|
}
|
|
|
|
Scene::~Scene() {
|
|
disconnect(this, &QGraphicsScene::selectionChanged, nullptr, nullptr);
|
|
cancelTextEditing();
|
|
if (const auto pending = base::take(_shapeTool.item)) {
|
|
QGraphicsScene::removeItem(pending.get());
|
|
}
|
|
QGraphicsScene::removeItem(_canvas.get());
|
|
for (const auto &item : items()) {
|
|
QGraphicsScene::removeItem(item.get());
|
|
}
|
|
}
|
|
|
|
} // namespace Editor
|