diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index c991f29a1..46d5e7ac7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/editor/editor.style b/Telegram/SourceFiles/editor/editor.style index 5b212f82d..1484cb3a1 100644 --- a/Telegram/SourceFiles/editor/editor.style +++ b/Telegram/SourceFiles/editor/editor.style @@ -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); diff --git a/Telegram/SourceFiles/editor/editor_paint.cpp b/Telegram/SourceFiles/editor/editor_paint.cpp index a1a41fc58..49625ed01 100644 --- a/Telegram/SourceFiles/editor/editor_paint.cpp +++ b/Telegram/SourceFiles/editor/editor_paint.cpp @@ -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 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 parent, PhotoModifications &modifications, - const QSize &imageSize) + const QSize &imageSize, + std::shared_ptr undoController) : RpWidget(parent) , _scene(EnsureScene(modifications)) , _view(base::make_unique_q(_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 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 item) const { + return !item->isVisible() && !isItemToRemove(item); +} + +bool Paint::isItemToRemove(not_null item) const { + return ranges::contains(_itemsToRemove, item.get()); +} + +void Paint::updateUndoState() { + _hasUndo = hasUndo(); + _hasRedo = hasRedo(); +} + +std::vector 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 diff --git a/Telegram/SourceFiles/editor/editor_paint.h b/Telegram/SourceFiles/editor/editor_paint.h index 8134f9290..da12053eb 100644 --- a/Telegram/SourceFiles/editor/editor_paint.h +++ b/Telegram/SourceFiles/editor/editor_paint.h @@ -16,29 +16,47 @@ class QGraphicsView; namespace Editor { +class UndoController; + // Paint control. class Paint final : public Ui::RpWidget { public: Paint( not_null parent, PhotoModifications &modifications, - const QSize &imageSize); + const QSize &imageSize, + std::shared_ptr undoController); [[nodiscard]] std::shared_ptr 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 item) const; + bool isItemHidden(not_null item) const; + + std::vector groups( + Qt::SortOrder order = Qt::DescendingOrder) const; const std::shared_ptr _scene; const base::unique_qptr _view; const QSize _imageSize; - int _startItemsCount = 0; + std::vector _previousItems; + QList _itemsToRemove; struct { QPointF lastPoint; @@ -48,6 +66,9 @@ private: QGraphicsItemGroup *group; } _brushData; + rpl::variable _hasUndo = true; + rpl::variable _hasRedo = true; + }; } // namespace Editor diff --git a/Telegram/SourceFiles/editor/photo_editor.cpp b/Telegram/SourceFiles/editor/photo_editor.cpp index 0e8a4dde0..0ef1ecabe 100644 --- a/Telegram/SourceFiles/editor/photo_editor.cpp +++ b/Telegram/SourceFiles/editor/photo_editor.cpp @@ -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()) , _content(base::make_unique_q( this, photo, - _modifications)) -, _controls(base::make_unique_q(this)) { + _modifications, + _undoController)) +, _controls(base::make_unique_q(this, _undoController)) { sizeValue( ) | rpl::start_with_next([=](const QSize &size) { const auto geometry = QRect(QPoint(), size); diff --git a/Telegram/SourceFiles/editor/photo_editor.h b/Telegram/SourceFiles/editor/photo_editor.h index 5b8a53475..6d133aeff 100644 --- a/Telegram/SourceFiles/editor/photo_editor.h +++ b/Telegram/SourceFiles/editor/photo_editor.h @@ -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; + base::unique_qptr _content; base::unique_qptr _controls; diff --git a/Telegram/SourceFiles/editor/photo_editor_content.cpp b/Telegram/SourceFiles/editor/photo_editor_content.cpp index 8ccd30f69..464134b09 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_content.cpp @@ -19,9 +19,14 @@ using Media::View::RotatedRect; PhotoEditorContent::PhotoEditorContent( not_null parent, std::shared_ptr photo, - PhotoModifications modifications) + PhotoModifications modifications, + std::shared_ptr undoController) : RpWidget(parent) -, _paint(base::make_unique_q(this, modifications, photo->size())) +, _paint(base::make_unique_q( + this, + modifications, + photo->size(), + std::move(undoController))) , _crop(base::make_unique_q(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(); diff --git a/Telegram/SourceFiles/editor/photo_editor_content.h b/Telegram/SourceFiles/editor/photo_editor_content.h index cf17c01df..d9610d51a 100644 --- a/Telegram/SourceFiles/editor/photo_editor_content.h +++ b/Telegram/SourceFiles/editor/photo_editor_content.h @@ -15,13 +15,15 @@ namespace Editor { class Crop; class Paint; +class UndoController; class PhotoEditorContent final : public Ui::RpWidget { public: PhotoEditorContent( not_null parent, std::shared_ptr photo, - PhotoModifications modifications); + PhotoModifications modifications, + std::shared_ptr undoController); void applyModifications(PhotoModifications modifications); void applyMode(const PhotoEditorMode &mode); diff --git a/Telegram/SourceFiles/editor/photo_editor_controls.cpp b/Telegram/SourceFiles/editor/photo_editor_controls.cpp index 79584e0d5..69cde03ab 100644 --- a/Telegram/SourceFiles/editor/photo_editor_controls.cpp +++ b/Telegram/SourceFiles/editor/photo_editor_controls.cpp @@ -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 parent, + std::shared_ptr 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 PhotoEditorControls::rotateRequests() const { diff --git a/Telegram/SourceFiles/editor/photo_editor_controls.h b/Telegram/SourceFiles/editor/photo_editor_controls.h index 3d2620383..c089cd162 100644 --- a/Telegram/SourceFiles/editor/photo_editor_controls.h +++ b/Telegram/SourceFiles/editor/photo_editor_controls.h @@ -19,11 +19,13 @@ namespace Editor { class EdgeButton; class HorizontalContainer; +class UndoController; class PhotoEditorControls final : public Ui::RpWidget { public: PhotoEditorControls( not_null parent, + std::shared_ptr undoController, bool doneControls = true); [[nodiscard]] rpl::producer rotateRequests() const; diff --git a/Telegram/SourceFiles/editor/undo_controller.cpp b/Telegram/SourceFiles/editor/undo_controller.cpp new file mode 100644 index 000000000..f9ca2b7f7 --- /dev/null +++ b/Telegram/SourceFiles/editor/undo_controller.cpp @@ -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 &&command) { + std::move( + command + ) | rpl::start_to_stream(_enable, _lifetime); +} + +void UndoController::setPerformRequestChanges(rpl::producer &&command) { + std::move( + command + ) | rpl::start_to_stream(_perform, _lifetime); +} + +rpl::producer UndoController::canPerformChanges() const { + return _enable.events(); +} + +rpl::producer UndoController::performRequestChanges() const { + return _perform.events(); +} + +} // namespace Editor diff --git a/Telegram/SourceFiles/editor/undo_controller.h b/Telegram/SourceFiles/editor/undo_controller.h new file mode 100644 index 000000000..516ee7d68 --- /dev/null +++ b/Telegram/SourceFiles/editor/undo_controller.h @@ -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 &&command); + void setPerformRequestChanges(rpl::producer &&command); + + [[nodiscard]] rpl::producer canPerformChanges() const; + [[nodiscard]] rpl::producer performRequestChanges() const; + +private: + + rpl::event_stream _perform; + rpl::event_stream _enable; + + rpl::lifetime _lifetime; + +}; + +} // namespace Editor