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_content.h
editor/photo_editor_controls.cpp editor/photo_editor_controls.cpp
editor/photo_editor_controls.h editor/photo_editor_controls.h
editor/undo_controller.cpp
editor/undo_controller.h
export/export_manager.cpp export/export_manager.cpp
export/export_manager.h export/export_manager.h
export/view/export_view_content.cpp export/view/export_view_content.cpp

View file

@ -17,6 +17,7 @@ photoEditorButtonIconFg: historyComposeIconFg;
photoEditorButtonIconFgOver: historyComposeIconFgOver; photoEditorButtonIconFgOver: historyComposeIconFgOver;
photoEditorButtonIconFgActive: historyComposeIconFgOver; photoEditorButtonIconFgActive: historyComposeIconFgOver;
photoEditorButtonIconFgInactive: menuFgDisabled;
photoEditorRotateButton: IconButton(historyAttach) { photoEditorRotateButton: IconButton(historyAttach) {
icon: icon {{ "photo_editor/rotate", photoEditorButtonIconFg }}; icon: icon {{ "photo_editor/rotate", photoEditorButtonIconFg }};
@ -39,4 +40,7 @@ photoEditorRedoButton: IconButton(historyAttach) {
iconOver: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgOver }}; 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); 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/editor_paint.h"
#include "editor/undo_controller.h"
#include "base/event_filter.h" #include "base/event_filter.h"
#include "styles/style_boxes.h" #include "styles/style_boxes.h"
@ -29,7 +30,7 @@ std::shared_ptr<QGraphicsScene> EnsureScene(PhotoModifications &mods) {
return mods.paint; return mods.paint;
} }
auto FilterItems(QGraphicsItem *i) { auto GroupsFilter(QGraphicsItem *i) {
return i->type() == QGraphicsItemGroup::Type; return i->type() == QGraphicsItemGroup::Type;
} }
@ -38,14 +39,16 @@ auto FilterItems(QGraphicsItem *i) {
Paint::Paint( Paint::Paint(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications, PhotoModifications &modifications,
const QSize &imageSize) const QSize &imageSize,
std::shared_ptr<UndoController> undoController)
: RpWidget(parent) : RpWidget(parent)
, _scene(EnsureScene(modifications)) , _scene(EnsureScene(modifications))
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this)) , _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
, _imageSize(imageSize) , _imageSize(imageSize) {
, _startItemsCount(itemsCount()) {
Expects(modifications.paint != nullptr); Expects(modifications.paint != nullptr);
keepResult();
_view->show(); _view->show();
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); _view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@ -54,6 +57,41 @@ Paint::Paint(
_scene->setSceneRect(0, 0, imageSize.width(), imageSize.height()); _scene->setSceneRect(0, 0, imageSize.width(), imageSize.height());
initDrawing(); 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) { void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
@ -95,6 +133,9 @@ void Paint::initDrawing() {
const auto &color = _brushData.color; const auto &color = _brushData.color;
const auto mousePoint = e->scenePos(); const auto mousePoint = e->scenePos();
if (isPress) { if (isPress) {
_hasUndo = true;
clearRedoList();
auto dot = _scene->addEllipse( auto dot = _scene->addEllipse(
mousePoint.x() - size / 2, mousePoint.x() - size / 2,
mousePoint.y() - size / 2, mousePoint.y() - size / 2,
@ -129,33 +170,83 @@ std::shared_ptr<QGraphicsScene> Paint::saveScene() const {
} }
void Paint::cancel() { void Paint::cancel() {
const auto items = _scene->items(Qt::AscendingOrder); const auto filtered = groups(Qt::AscendingOrder);
const auto filtered = ranges::views::all(
items
) | ranges::views::filter(FilterItems) | ranges::to_vector;
if (filtered.empty()) { if (filtered.empty()) {
return; return;
} }
for (auto i = 0; i < filtered.size(); i++) { for (const auto &group : filtered) {
const auto &item = filtered[i]; const auto it = ranges::find(
if (i < _startItemsCount) { _previousItems,
if (!item->isVisible()) { group,
item->show(); &SavedItem::item);
} if (it == end(_previousItems)) {
_scene->removeItem(group);
} else { } else {
_scene->removeItem(item); it->item->setVisible(!it->undid);
} }
} }
_itemsToRemove.clear();
} }
void Paint::keepResult() { 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 { bool Paint::hasUndo() const {
return ranges::count_if(_scene->items(), FilterItems); 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 } // namespace Editor

View file

@ -16,29 +16,47 @@ class QGraphicsView;
namespace Editor { namespace Editor {
class UndoController;
// Paint control. // Paint control.
class Paint final : public Ui::RpWidget { class Paint final : public Ui::RpWidget {
public: public:
Paint( Paint(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications, PhotoModifications &modifications,
const QSize &imageSize); const QSize &imageSize,
std::shared_ptr<UndoController> undoController);
[[nodiscard]] std::shared_ptr<QGraphicsScene> saveScene() const; [[nodiscard]] std::shared_ptr<QGraphicsScene> saveScene() const;
void applyTransform(QRect geometry, int angle, bool flipped); void applyTransform(QRect geometry, int angle, bool flipped);
void cancel(); void cancel();
void keepResult(); void keepResult();
void updateUndoState();
private: private:
struct SavedItem {
QGraphicsItem *item;
bool undid = false;
};
void initDrawing(); 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 std::shared_ptr<QGraphicsScene> _scene;
const base::unique_qptr<QGraphicsView> _view; const base::unique_qptr<QGraphicsView> _view;
const QSize _imageSize; const QSize _imageSize;
int _startItemsCount = 0; std::vector<SavedItem> _previousItems;
QList<QGraphicsItem*> _itemsToRemove;
struct { struct {
QPointF lastPoint; QPointF lastPoint;
@ -48,6 +66,9 @@ private:
QGraphicsItemGroup *group; QGraphicsItemGroup *group;
} _brushData; } _brushData;
rpl::variable<bool> _hasUndo = true;
rpl::variable<bool> _hasRedo = true;
}; };
} // namespace Editor } // 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_content.h"
#include "editor/photo_editor_controls.h" #include "editor/photo_editor_controls.h"
#include "editor/undo_controller.h"
#include "styles/style_editor.h" #include "styles/style_editor.h"
namespace Editor { namespace Editor {
@ -19,11 +20,13 @@ PhotoEditor::PhotoEditor(
PhotoModifications modifications) PhotoModifications modifications)
: RpWidget(parent) : RpWidget(parent)
, _modifications(std::move(modifications)) , _modifications(std::move(modifications))
, _undoController(std::make_shared<UndoController>())
, _content(base::make_unique_q<PhotoEditorContent>( , _content(base::make_unique_q<PhotoEditorContent>(
this, this,
photo, photo,
_modifications)) _modifications,
, _controls(base::make_unique_q<PhotoEditorControls>(this)) { _undoController))
, _controls(base::make_unique_q<PhotoEditorControls>(this, _undoController)) {
sizeValue( sizeValue(
) | rpl::start_with_next([=](const QSize &size) { ) | rpl::start_with_next([=](const QSize &size) {
const auto geometry = QRect(QPoint(), size); const auto geometry = QRect(QPoint(), size);

View file

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

View file

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

View file

@ -15,13 +15,15 @@ namespace Editor {
class Crop; class Crop;
class Paint; class Paint;
class UndoController;
class PhotoEditorContent final : public Ui::RpWidget { class PhotoEditorContent final : public Ui::RpWidget {
public: public:
PhotoEditorContent( PhotoEditorContent(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
std::shared_ptr<QPixmap> photo, std::shared_ptr<QPixmap> photo,
PhotoModifications modifications); PhotoModifications modifications,
std::shared_ptr<UndoController> undoController);
void applyModifications(PhotoModifications modifications); void applyModifications(PhotoModifications modifications);
void applyMode(const PhotoEditorMode &mode); 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/photo_editor_controls.h"
#include "editor/undo_controller.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "ui/image/image_prepare.h" #include "ui/image/image_prepare.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
@ -133,6 +134,7 @@ void HorizontalContainer::updateChildrenPosition() {
PhotoEditorControls::PhotoEditorControls( PhotoEditorControls::PhotoEditorControls(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
std::shared_ptr<UndoController> undoController,
bool doneControls) bool doneControls)
: RpWidget(parent) : RpWidget(parent)
, _bg(st::mediaviewSaveMsgBg) , _bg(st::mediaviewSaveMsgBg)
@ -213,6 +215,26 @@ PhotoEditorControls::PhotoEditorControls(
}, lifetime()); }, 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 { rpl::producer<int> PhotoEditorControls::rotateRequests() const {

View file

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