Added ability to undo and to redo paint actions in photo editor.

This commit is contained in:
23rd 2021-03-14 12:42:18 +03:00
parent 8eca57f419
commit 4849376347
12 changed files with 266 additions and 30 deletions

View File

@ -520,6 +520,8 @@ PRIVATE
editor/photo_editor_content.h
editor/photo_editor_controls.cpp
editor/photo_editor_controls.h
editor/undo_controller.cpp
editor/undo_controller.h
export/export_manager.cpp
export/export_manager.h
export/view/export_view_content.cpp

View File

@ -17,6 +17,7 @@ photoEditorButtonIconFg: historyComposeIconFg;
photoEditorButtonIconFgOver: historyComposeIconFgOver;
photoEditorButtonIconFgActive: historyComposeIconFgOver;
photoEditorButtonIconFgInactive: menuFgDisabled;
photoEditorRotateButton: IconButton(historyAttach) {
icon: icon {{ "photo_editor/rotate", photoEditorButtonIconFg }};
@ -39,4 +40,7 @@ photoEditorRedoButton: IconButton(historyAttach) {
iconOver: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgOver }};
}
photoEditorUndoButtonInactive: icon {{ "photo_editor/undo", photoEditorButtonIconFgInactive }};
photoEditorRedoButtonInactive: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgInactive }};
photoEditorTextButtonPadding: margins(10px, 0px, 10px, 0px);

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_paint.h"
#include "editor/undo_controller.h"
#include "base/event_filter.h"
#include "styles/style_boxes.h"
@ -29,7 +30,7 @@ std::shared_ptr<QGraphicsScene> EnsureScene(PhotoModifications &mods) {
return mods.paint;
}
auto FilterItems(QGraphicsItem *i) {
auto GroupsFilter(QGraphicsItem *i) {
return i->type() == QGraphicsItemGroup::Type;
}
@ -38,14 +39,16 @@ auto FilterItems(QGraphicsItem *i) {
Paint::Paint(
not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications,
const QSize &imageSize)
const QSize &imageSize,
std::shared_ptr<UndoController> undoController)
: RpWidget(parent)
, _scene(EnsureScene(modifications))
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
, _imageSize(imageSize)
, _startItemsCount(itemsCount()) {
, _imageSize(imageSize) {
Expects(modifications.paint != nullptr);
keepResult();
_view->show();
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@ -54,6 +57,41 @@ Paint::Paint(
_scene->setSceneRect(0, 0, imageSize.width(), imageSize.height());
initDrawing();
// Undo / Redo.
undoController->performRequestChanges(
) | rpl::start_with_next([=](const Undo &command) {
const auto isUndo = (command == Undo::Undo);
const auto filtered = groups(isUndo
? Qt::DescendingOrder
: Qt::AscendingOrder);
auto proj = [&](QGraphicsItem *i) {
return isUndo ? i->isVisible() : isItemHidden(i);
};
const auto it = ranges::find_if(filtered, std::move(proj));
if (it != filtered.end()) {
(*it)->setVisible(!isUndo);
}
_hasUndo = hasUndo();
_hasRedo = hasRedo();
}, lifetime());
undoController->setCanPerformChanges(rpl::merge(
_hasUndo.value() | rpl::map([](bool enable) {
return UndoController::EnableRequest{
.command = Undo::Undo,
.enable = enable,
};
}),
_hasRedo.value() | rpl::map([](bool enable) {
return UndoController::EnableRequest{
.command = Undo::Redo,
.enable = enable,
};
})));
}
void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
@ -95,6 +133,9 @@ void Paint::initDrawing() {
const auto &color = _brushData.color;
const auto mousePoint = e->scenePos();
if (isPress) {
_hasUndo = true;
clearRedoList();
auto dot = _scene->addEllipse(
mousePoint.x() - size / 2,
mousePoint.y() - size / 2,
@ -129,33 +170,83 @@ std::shared_ptr<QGraphicsScene> Paint::saveScene() const {
}
void Paint::cancel() {
const auto items = _scene->items(Qt::AscendingOrder);
const auto filtered = ranges::views::all(
items
) | ranges::views::filter(FilterItems) | ranges::to_vector;
const auto filtered = groups(Qt::AscendingOrder);
if (filtered.empty()) {
return;
}
for (auto i = 0; i < filtered.size(); i++) {
const auto &item = filtered[i];
if (i < _startItemsCount) {
if (!item->isVisible()) {
item->show();
}
for (const auto &group : filtered) {
const auto it = ranges::find(
_previousItems,
group,
&SavedItem::item);
if (it == end(_previousItems)) {
_scene->removeItem(group);
} else {
_scene->removeItem(item);
it->item->setVisible(!it->undid);
}
}
_itemsToRemove.clear();
}
void Paint::keepResult() {
_startItemsCount = itemsCount();
for (const auto &item : _itemsToRemove) {
_scene->removeItem(item);
}
const auto items = _scene->items();
_previousItems = ranges::views::all(
items
) | ranges::views::transform([=](QGraphicsItem *i) -> SavedItem {
return { i, !i->isVisible() };
}) | ranges::to_vector;
}
int Paint::itemsCount() const {
return ranges::count_if(_scene->items(), FilterItems);
bool Paint::hasUndo() const {
return ranges::any_of(groups(), &QGraphicsItem::isVisible);
}
bool Paint::hasRedo() const {
return ranges::any_of(
groups(),
[=](QGraphicsItem *i) { return isItemHidden(i); });
}
void Paint::clearRedoList() {
const auto items = groups(Qt::AscendingOrder);
auto &&filtered = ranges::views::all(
items
) | ranges::views::filter(
[=](QGraphicsItem *i) { return isItemHidden(i); }
);
ranges::for_each(std::move(filtered), [&](QGraphicsItem *item) {
item->hide();
_itemsToRemove.push_back(item);
});
_hasRedo = false;
}
bool Paint::isItemHidden(not_null<QGraphicsItem*> item) const {
return !item->isVisible() && !isItemToRemove(item);
}
bool Paint::isItemToRemove(not_null<QGraphicsItem*> item) const {
return ranges::contains(_itemsToRemove, item.get());
}
void Paint::updateUndoState() {
_hasUndo = hasUndo();
_hasRedo = hasRedo();
}
std::vector<QGraphicsItem*> Paint::groups(Qt::SortOrder order) const {
const auto items = _scene->items(order);
return ranges::views::all(
items
) | ranges::views::filter(GroupsFilter) | ranges::to_vector;
}
} // namespace Editor

View File

@ -16,29 +16,47 @@ class QGraphicsView;
namespace Editor {
class UndoController;
// Paint control.
class Paint final : public Ui::RpWidget {
public:
Paint(
not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications,
const QSize &imageSize);
const QSize &imageSize,
std::shared_ptr<UndoController> undoController);
[[nodiscard]] std::shared_ptr<QGraphicsScene> saveScene() const;
void applyTransform(QRect geometry, int angle, bool flipped);
void cancel();
void keepResult();
void updateUndoState();
private:
struct SavedItem {
QGraphicsItem *item;
bool undid = false;
};
void initDrawing();
int itemsCount() const;
bool hasUndo() const;
bool hasRedo() const;
void clearRedoList();
bool isItemToRemove(not_null<QGraphicsItem*> item) const;
bool isItemHidden(not_null<QGraphicsItem*> item) const;
std::vector<QGraphicsItem*> groups(
Qt::SortOrder order = Qt::DescendingOrder) const;
const std::shared_ptr<QGraphicsScene> _scene;
const base::unique_qptr<QGraphicsView> _view;
const QSize _imageSize;
int _startItemsCount = 0;
std::vector<SavedItem> _previousItems;
QList<QGraphicsItem*> _itemsToRemove;
struct {
QPointF lastPoint;
@ -48,6 +66,9 @@ private:
QGraphicsItemGroup *group;
} _brushData;
rpl::variable<bool> _hasUndo = true;
rpl::variable<bool> _hasRedo = true;
};
} // namespace Editor

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/photo_editor_content.h"
#include "editor/photo_editor_controls.h"
#include "editor/undo_controller.h"
#include "styles/style_editor.h"
namespace Editor {
@ -19,11 +20,13 @@ PhotoEditor::PhotoEditor(
PhotoModifications modifications)
: RpWidget(parent)
, _modifications(std::move(modifications))
, _undoController(std::make_shared<UndoController>())
, _content(base::make_unique_q<PhotoEditorContent>(
this,
photo,
_modifications))
, _controls(base::make_unique_q<PhotoEditorControls>(this)) {
_modifications,
_undoController))
, _controls(base::make_unique_q<PhotoEditorControls>(this, _undoController)) {
sizeValue(
) | rpl::start_with_next([=](const QSize &size) {
const auto geometry = QRect(QPoint(), size);

View File

@ -17,6 +17,7 @@ namespace Editor {
class PhotoEditorContent;
class PhotoEditorControls;
class UndoController;
class PhotoEditor final : public Ui::RpWidget {
public:
@ -32,6 +33,8 @@ private:
PhotoModifications _modifications;
const std::shared_ptr<UndoController> _undoController;
base::unique_qptr<PhotoEditorContent> _content;
base::unique_qptr<PhotoEditorControls> _controls;

View File

@ -19,9 +19,14 @@ using Media::View::RotatedRect;
PhotoEditorContent::PhotoEditorContent(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<QPixmap> photo,
PhotoModifications modifications)
PhotoModifications modifications,
std::shared_ptr<UndoController> undoController)
: RpWidget(parent)
, _paint(base::make_unique_q<Paint>(this, modifications, photo->size()))
, _paint(base::make_unique_q<Paint>(
this,
modifications,
photo->size(),
std::move(undoController)))
, _crop(base::make_unique_q<Crop>(this, modifications, photo->size()))
, _photo(photo)
, _modifications(modifications) {
@ -88,9 +93,7 @@ void PhotoEditorContent::applyModifications(
void PhotoEditorContent::save(PhotoModifications &modifications) {
modifications.crop = _crop->saveCropRect(_imageRect, _photo->rect());
if (!modifications.paint) {
modifications.paint = _paint->saveScene();
}
_paint->keepResult();
}
void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
@ -98,6 +101,9 @@ void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
_crop->setVisible(isTransform);
_paint->setAttribute(Qt::WA_TransparentForMouseEvents, isTransform);
if (!isTransform) {
_paint->updateUndoState();
}
if (mode.action == PhotoEditorMode::Action::Discard) {
_paint->cancel();

View File

@ -15,13 +15,15 @@ namespace Editor {
class Crop;
class Paint;
class UndoController;
class PhotoEditorContent final : public Ui::RpWidget {
public:
PhotoEditorContent(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<QPixmap> photo,
PhotoModifications modifications);
PhotoModifications modifications,
std::shared_ptr<UndoController> undoController);
void applyModifications(PhotoModifications modifications);
void applyMode(const PhotoEditorMode &mode);

View File

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_controls.h"
#include "editor/undo_controller.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/widgets/buttons.h"
@ -133,6 +134,7 @@ void HorizontalContainer::updateChildrenPosition() {
PhotoEditorControls::PhotoEditorControls(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<UndoController> undoController,
bool doneControls)
: RpWidget(parent)
, _bg(st::mediaviewSaveMsgBg)
@ -213,6 +215,26 @@ PhotoEditorControls::PhotoEditorControls(
}, lifetime());
undoController->setPerformRequestChanges(rpl::merge(
_undoButton->clicks() | rpl::map_to(Undo::Undo),
_redoButton->clicks() | rpl::map_to(Undo::Redo)));
undoController->canPerformChanges(
) | rpl::start_with_next([=](const UndoController::EnableRequest &r) {
const auto isUndo = (r.command == Undo::Undo);
const auto &button = isUndo ? _undoButton : _redoButton;
button->setAttribute(Qt::WA_TransparentForMouseEvents, !r.enable);
if (!r.enable) {
button->clearState();
}
button->setIconOverride(r.enable
? nullptr
: isUndo
? &st::photoEditorUndoButtonInactive
: &st::photoEditorRedoButtonInactive);
}, lifetime());
}
rpl::producer<int> PhotoEditorControls::rotateRequests() const {

View File

@ -19,11 +19,13 @@ namespace Editor {
class EdgeButton;
class HorizontalContainer;
class UndoController;
class PhotoEditorControls final : public Ui::RpWidget {
public:
PhotoEditorControls(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<UndoController> undoController,
bool doneControls = true);
[[nodiscard]] rpl::producer<int> rotateRequests() const;

View File

@ -0,0 +1,39 @@
/*
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/undo_controller.h"
namespace Editor {
namespace {
using EnableRequest = UndoController::EnableRequest;
} // namespace
UndoController::UndoController() {
}
void UndoController::setCanPerformChanges(
rpl::producer<EnableRequest> &&command) {
std::move(
command
) | rpl::start_to_stream(_enable, _lifetime);
}
void UndoController::setPerformRequestChanges(rpl::producer<Undo> &&command) {
std::move(
command
) | rpl::start_to_stream(_perform, _lifetime);
}
rpl::producer<EnableRequest> UndoController::canPerformChanges() const {
return _enable.events();
}
rpl::producer<Undo> UndoController::performRequestChanges() const {
return _perform.events();
}
} // namespace Editor

View File

@ -0,0 +1,41 @@
/*
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
namespace Editor {
enum class Undo {
Undo,
Redo,
};
class UndoController final {
public:
struct EnableRequest {
Undo command = Undo::Undo;
bool enable = true;
};
UndoController();
void setCanPerformChanges(rpl::producer<EnableRequest> &&command);
void setPerformRequestChanges(rpl::producer<Undo> &&command);
[[nodiscard]] rpl::producer<EnableRequest> canPerformChanges() const;
[[nodiscard]] rpl::producer<Undo> performRequestChanges() const;
private:
rpl::event_stream<Undo> _perform;
rpl::event_stream<EnableRequest> _enable;
rpl::lifetime _lifetime;
};
} // namespace Editor