From 1c27568ab0373c35bd33d0956658e8473efe181d Mon Sep 17 00:00:00 2001 From: BenJilks Date: Sat, 17 Oct 2020 16:47:34 +0000 Subject: [PATCH] PixelPaint: Undo and redo actions The most used feature of any image editor, undo. Each tool now notifies the ImageEditor that they completed an action, where it'll take a snapshot if its current state. For now, a snapshot is just a copy of the whole image and its layers. There's a hard limit on the amount of actions it stores. --- Applications/PixelPaint/BrushTool.cpp | 9 +++ Applications/PixelPaint/BrushTool.h | 2 + Applications/PixelPaint/BucketTool.cpp | 1 + Applications/PixelPaint/CMakeLists.txt | 1 + Applications/PixelPaint/EllipseTool.cpp | 1 + Applications/PixelPaint/EraseTool.cpp | 7 +++ Applications/PixelPaint/EraseTool.h | 1 + Applications/PixelPaint/History.cpp | 73 +++++++++++++++++++++++ Applications/PixelPaint/History.h | 54 +++++++++++++++++ Applications/PixelPaint/Image.cpp | 28 +++++++++ Applications/PixelPaint/Image.h | 4 ++ Applications/PixelPaint/ImageEditor.cpp | 35 +++++++++++ Applications/PixelPaint/ImageEditor.h | 7 +++ Applications/PixelPaint/Layer.cpp | 10 ++++ Applications/PixelPaint/Layer.h | 1 + Applications/PixelPaint/LineTool.cpp | 1 + Applications/PixelPaint/MoveTool.cpp | 1 + Applications/PixelPaint/PenTool.cpp | 4 +- Applications/PixelPaint/RectangleTool.cpp | 1 + Applications/PixelPaint/SprayTool.cpp | 5 +- Applications/PixelPaint/main.cpp | 12 ++++ 21 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 Applications/PixelPaint/History.cpp create mode 100644 Applications/PixelPaint/History.h diff --git a/Applications/PixelPaint/BrushTool.cpp b/Applications/PixelPaint/BrushTool.cpp index 3caebcb60f0..0d099a80877 100644 --- a/Applications/PixelPaint/BrushTool.cpp +++ b/Applications/PixelPaint/BrushTool.cpp @@ -62,6 +62,15 @@ void BrushTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEve draw_line(layer.bitmap(), m_editor->color_for(event), m_last_position, event.position()); layer.did_modify_bitmap(*m_editor->image()); m_last_position = event.position(); + m_was_drawing = true; +} + +void BrushTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) +{ + if (m_was_drawing) { + m_editor->did_complete_action(); + m_was_drawing = false; + } } void BrushTool::draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gfx::IntPoint& point) diff --git a/Applications/PixelPaint/BrushTool.h b/Applications/PixelPaint/BrushTool.h index 33b27fce4e8..9c42932413c 100644 --- a/Applications/PixelPaint/BrushTool.h +++ b/Applications/PixelPaint/BrushTool.h @@ -37,12 +37,14 @@ public: virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; + virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual GUI::Widget* get_properties_widget() override; private: RefPtr m_properties_widget; int m_size { 20 }; int m_hardness { 80 }; + bool m_was_drawing { false }; Gfx::IntPoint m_last_position; virtual const char* class_name() const override { return "BrushTool"; } diff --git a/Applications/PixelPaint/BucketTool.cpp b/Applications/PixelPaint/BucketTool.cpp index 11d0cf639df..09e2e515450 100644 --- a/Applications/PixelPaint/BucketTool.cpp +++ b/Applications/PixelPaint/BucketTool.cpp @@ -101,6 +101,7 @@ void BucketTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEv flood_fill(layer.bitmap(), event.position(), target_color, m_editor->color_for(event), m_threshold); layer.did_modify_bitmap(*m_editor->image()); + m_editor->did_complete_action(); } GUI::Widget* BucketTool::get_properties_widget() diff --git a/Applications/PixelPaint/CMakeLists.txt b/Applications/PixelPaint/CMakeLists.txt index c1ea822c454..4a5396d5d08 100644 --- a/Applications/PixelPaint/CMakeLists.txt +++ b/Applications/PixelPaint/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES CreateNewLayerDialog.cpp EllipseTool.cpp EraseTool.cpp + History.cpp Image.cpp ImageEditor.cpp Layer.cpp diff --git a/Applications/PixelPaint/EllipseTool.cpp b/Applications/PixelPaint/EllipseTool.cpp index e4914f03b2b..0501e2fb963 100644 --- a/Applications/PixelPaint/EllipseTool.cpp +++ b/Applications/PixelPaint/EllipseTool.cpp @@ -75,6 +75,7 @@ void EllipseTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEve draw_using(painter, Gfx::IntRect::from_two_points(m_ellipse_start_position, m_ellipse_end_position)); m_drawing_button = GUI::MouseButton::None; m_editor->update(); + m_editor->did_complete_action(); } } diff --git a/Applications/PixelPaint/EraseTool.cpp b/Applications/PixelPaint/EraseTool.cpp index 77b76c52d34..19d1482ba8f 100644 --- a/Applications/PixelPaint/EraseTool.cpp +++ b/Applications/PixelPaint/EraseTool.cpp @@ -72,6 +72,13 @@ void EraseTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEve } } +void EraseTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&) +{ + if (event.button() != GUI::MouseButton::Left && event.button() != GUI::MouseButton::Right) + return; + m_editor->did_complete_action(); +} + void EraseTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event) { if (!m_context_menu) { diff --git a/Applications/PixelPaint/EraseTool.h b/Applications/PixelPaint/EraseTool.h index 772479fb3f3..b41f628a8fc 100644 --- a/Applications/PixelPaint/EraseTool.h +++ b/Applications/PixelPaint/EraseTool.h @@ -40,6 +40,7 @@ public: virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; + virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override; private: diff --git a/Applications/PixelPaint/History.cpp b/Applications/PixelPaint/History.cpp new file mode 100644 index 00000000000..2295a85bfea --- /dev/null +++ b/Applications/PixelPaint/History.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020, Ben Jilks + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "History.h" +#include "Image.h" +#include "Layer.h" +#include +#include + +namespace PixelPaint { + +void History::on_action(const Image& image) +{ + m_snapshots.shrink(m_snapshots.size() - m_current_index_back_into_history); + m_current_index_back_into_history = 0; + m_snapshots.append(image.take_snapshot()); + if (m_snapshots.size() > s_max_size) + m_snapshots.take_first(); +} + +bool History::undo(Image& image) +{ + if (m_snapshots.size() - m_current_index_back_into_history - 1 <= 0) + return false; + + m_current_index_back_into_history += 1; + const Image& last_snapshot = *m_snapshots[m_snapshots.size() - m_current_index_back_into_history - 1]; + image.restore_snapshot(last_snapshot); + return true; +} + +bool History::redo(Image& image) +{ + if (m_current_index_back_into_history <= 0) + return false; + + const Image& last_snapshot = *m_snapshots[m_snapshots.size() - m_current_index_back_into_history]; + m_current_index_back_into_history -= 1; + image.restore_snapshot(last_snapshot); + return true; +} + +void History::reset(const Image& image) +{ + m_snapshots.clear(); + m_current_index_back_into_history = 0; + on_action(image); +} + +} diff --git a/Applications/PixelPaint/History.h b/Applications/PixelPaint/History.h new file mode 100644 index 00000000000..f2ec352ff22 --- /dev/null +++ b/Applications/PixelPaint/History.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, Ben Jilks + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace PixelPaint { + +class Image; + +class History { + AK_MAKE_NONCOPYABLE(History); + AK_MAKE_NONMOVABLE(History); + +public: + History() = default; + + void on_action(const Image&); + bool undo(Image&); + bool redo(Image&); + void reset(const Image&); + +private: + static constexpr int s_max_size = 50; + Vector> m_snapshots; + int m_current_index_back_into_history { 0 }; +}; + +} diff --git a/Applications/PixelPaint/Image.cpp b/Applications/PixelPaint/Image.cpp index 84d8d74aa71..470bbbf9857 100644 --- a/Applications/PixelPaint/Image.cpp +++ b/Applications/PixelPaint/Image.cpp @@ -75,6 +75,28 @@ void Image::add_layer(NonnullRefPtr layer) did_modify_layer_stack(); } +RefPtr Image::take_snapshot() const +{ + auto snapshot = create_with_size(m_size); + for (const auto& layer : m_layers) + snapshot->add_layer(*Layer::create_snapshot(*snapshot, layer)); + return snapshot; +} + +void Image::restore_snapshot(const Image& snapshot) +{ + m_layers.clear(); + select_layer(nullptr); + for (const auto& snapshot_layer : snapshot.m_layers) { + auto layer = Layer::create_snapshot(*this, snapshot_layer); + if (layer->is_selected()) + select_layer(layer.ptr()); + add_layer(*layer); + } + + did_modify_layer_stack(); +} + size_t Image::index_of(const Layer& layer) const { for (size_t i = 0; i < m_layers.size(); ++i) { @@ -157,6 +179,12 @@ void Image::remove_layer(Layer& layer) did_modify_layer_stack(); } +void Image::select_layer(Layer* layer) +{ + for (auto* client : m_clients) + client->image_select_layer(layer); +} + void Image::add_client(ImageClient& client) { ASSERT(!m_clients.contains(&client)); diff --git a/Applications/PixelPaint/Image.h b/Applications/PixelPaint/Image.h index e723c646a11..fc944c17d26 100644 --- a/Applications/PixelPaint/Image.h +++ b/Applications/PixelPaint/Image.h @@ -47,6 +47,7 @@ public: virtual void image_did_modify_layer(size_t) { } virtual void image_did_modify_layer_stack() { } virtual void image_did_change() { } + virtual void image_select_layer(Layer*) { } }; class Image : public RefCounted { @@ -61,6 +62,8 @@ public: Gfx::IntRect rect() const { return { {}, m_size }; } void add_layer(NonnullRefPtr); + RefPtr take_snapshot() const; + void restore_snapshot(const Image&); void paint_into(GUI::Painter&, const Gfx::IntRect& dest_rect); @@ -70,6 +73,7 @@ public: void move_layer_down(Layer&); void change_layer_index(size_t old_index, size_t new_index); void remove_layer(Layer&); + void select_layer(Layer*); void add_client(ImageClient&); void remove_client(ImageClient&); diff --git a/Applications/PixelPaint/ImageEditor.cpp b/Applications/PixelPaint/ImageEditor.cpp index 3f0bfb72b7b..1e8d87184e4 100644 --- a/Applications/PixelPaint/ImageEditor.cpp +++ b/Applications/PixelPaint/ImageEditor.cpp @@ -51,12 +51,42 @@ void ImageEditor::set_image(RefPtr image) m_image->remove_client(*this); m_image = move(image); + m_history.reset(*m_image); update(); if (m_image) m_image->add_client(*this); } +void ImageEditor::did_complete_action() +{ + if (!m_image) + return; + m_history.on_action(*m_image); +} + +bool ImageEditor::undo() +{ + if (!m_image) + return false; + if (m_history.undo(*m_image)) { + layers_did_change(); + return true; + } + return false; +} + +bool ImageEditor::redo() +{ + if (!m_image) + return false; + if (m_history.redo(*m_image)) { + layers_did_change(); + return true; + } + return false; +} + void ImageEditor::paint_event(GUI::PaintEvent& event) { GUI::Frame::paint_event(event); @@ -371,4 +401,9 @@ void ImageEditor::image_did_change() update(); } +void ImageEditor::image_select_layer(Layer* layer) +{ + set_active_layer(layer); +} + } diff --git a/Applications/PixelPaint/ImageEditor.h b/Applications/PixelPaint/ImageEditor.h index 4e343e56560..0873cd5c1c0 100644 --- a/Applications/PixelPaint/ImageEditor.h +++ b/Applications/PixelPaint/ImageEditor.h @@ -26,6 +26,7 @@ #pragma once +#include "History.h" #include "Image.h" #include #include @@ -54,6 +55,10 @@ public: Tool* active_tool() { return m_active_tool; } void set_active_tool(Tool*); + void did_complete_action(); + bool undo(); + bool redo(); + void layers_did_change(); Layer* layer_at_editor_position(const Gfx::IntPoint&); @@ -94,6 +99,7 @@ private: virtual void resize_event(GUI::ResizeEvent&) override; virtual void image_did_change() override; + virtual void image_select_layer(Layer*) override; GUI::MouseEvent event_adjusted_for_layer(const GUI::MouseEvent&, const Layer&) const; GUI::MouseEvent event_with_pan_and_scale_applied(const GUI::MouseEvent&) const; @@ -102,6 +108,7 @@ private: RefPtr m_image; RefPtr m_active_layer; + History m_history; Tool* m_active_tool { nullptr }; diff --git a/Applications/PixelPaint/Layer.cpp b/Applications/PixelPaint/Layer.cpp index b8dc2cf763b..e607113c32c 100644 --- a/Applications/PixelPaint/Layer.cpp +++ b/Applications/PixelPaint/Layer.cpp @@ -52,6 +52,16 @@ RefPtr Layer::create_with_bitmap(Image& image, const Gfx::Bitmap& bitmap, return adopt(*new Layer(image, bitmap, name)); } +RefPtr Layer::create_snapshot(Image& image, const Layer& layer) +{ + auto snapshot = create_with_bitmap(image, *layer.bitmap().clone(), layer.name()); + snapshot->set_opacity_percent(layer.opacity_percent()); + snapshot->set_visible(layer.is_visible()); + snapshot->set_selected(layer.is_selected()); + snapshot->set_location(layer.location()); + return snapshot; +} + Layer::Layer(Image& image, const Gfx::IntSize& size, const String& name) : m_image(image) , m_name(name) diff --git a/Applications/PixelPaint/Layer.h b/Applications/PixelPaint/Layer.h index 5e91ad9ad68..b3b7d4d603b 100644 --- a/Applications/PixelPaint/Layer.h +++ b/Applications/PixelPaint/Layer.h @@ -46,6 +46,7 @@ class Layer public: static RefPtr create_with_size(Image&, const Gfx::IntSize&, const String& name); static RefPtr create_with_bitmap(Image&, const Gfx::Bitmap&, const String& name); + static RefPtr create_snapshot(Image&, const Layer&); ~Layer() { } diff --git a/Applications/PixelPaint/LineTool.cpp b/Applications/PixelPaint/LineTool.cpp index 8dd2d0a4f1b..7c3e37e8360 100644 --- a/Applications/PixelPaint/LineTool.cpp +++ b/Applications/PixelPaint/LineTool.cpp @@ -78,6 +78,7 @@ void LineTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent& painter.draw_line(m_line_start_position, m_line_end_position, m_editor->color_for(m_drawing_button), m_thickness); m_drawing_button = GUI::MouseButton::None; layer.did_modify_bitmap(*m_editor->image()); + m_editor->did_complete_action(); } } diff --git a/Applications/PixelPaint/MoveTool.cpp b/Applications/PixelPaint/MoveTool.cpp index cf6cea9975b..ebeba5ba89f 100644 --- a/Applications/PixelPaint/MoveTool.cpp +++ b/Applications/PixelPaint/MoveTool.cpp @@ -70,6 +70,7 @@ void MoveTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&) return; m_layer_being_moved = nullptr; m_editor->window()->set_cursor(Gfx::StandardCursor::None); + m_editor->did_complete_action(); } void MoveTool::on_keydown(GUI::KeyEvent& event) diff --git a/Applications/PixelPaint/PenTool.cpp b/Applications/PixelPaint/PenTool.cpp index 63512b4010e..25896623e62 100644 --- a/Applications/PixelPaint/PenTool.cpp +++ b/Applications/PixelPaint/PenTool.cpp @@ -57,8 +57,10 @@ void PenTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent void PenTool::on_mouseup(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&) { - if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) + if (event.button() == GUI::MouseButton::Left || event.button() == GUI::MouseButton::Right) { m_last_drawing_event_position = { -1, -1 }; + m_editor->did_complete_action(); + } } void PenTool::on_mousemove(Layer& layer, GUI::MouseEvent& event, GUI::MouseEvent&) diff --git a/Applications/PixelPaint/RectangleTool.cpp b/Applications/PixelPaint/RectangleTool.cpp index 4eaffd29bb4..a5b023f18b8 100644 --- a/Applications/PixelPaint/RectangleTool.cpp +++ b/Applications/PixelPaint/RectangleTool.cpp @@ -82,6 +82,7 @@ void RectangleTool::on_mouseup(Layer& layer, GUI::MouseEvent& event, GUI::MouseE draw_using(painter, rect); m_drawing_button = GUI::MouseButton::None; layer.did_modify_bitmap(*m_editor->image()); + m_editor->did_complete_action(); } } diff --git a/Applications/PixelPaint/SprayTool.cpp b/Applications/PixelPaint/SprayTool.cpp index 72e6fc7f039..9ec5c128944 100644 --- a/Applications/PixelPaint/SprayTool.cpp +++ b/Applications/PixelPaint/SprayTool.cpp @@ -104,7 +104,10 @@ void SprayTool::on_mousemove(Layer&, GUI::MouseEvent& event, GUI::MouseEvent&) void SprayTool::on_mouseup(Layer&, GUI::MouseEvent&, GUI::MouseEvent&) { - m_timer->stop(); + if (m_timer->is_active()) { + m_timer->stop(); + m_editor->did_complete_action(); + } } void SprayTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event) diff --git a/Applications/PixelPaint/main.cpp b/Applications/PixelPaint/main.cpp index 9c57483762c..e979f3d7400 100644 --- a/Applications/PixelPaint/main.cpp +++ b/Applications/PixelPaint/main.cpp @@ -144,6 +144,18 @@ int main(int argc, char** argv) edit_menu.add_action(paste_action); + auto undo_action = GUI::CommonActions::make_undo_action([&](auto&) { + ASSERT(image_editor.image()); + image_editor.undo(); + }); + edit_menu.add_action(undo_action); + + auto redo_action = GUI::CommonActions::make_redo_action([&](auto&) { + ASSERT(image_editor.image()); + image_editor.redo(); + }); + edit_menu.add_action(redo_action); + auto& tool_menu = menubar->add_menu("Tool"); toolbox.for_each_tool([&](auto& tool) { if (tool.action())