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.
This commit is contained in:
BenJilks 2020-10-17 16:47:34 +00:00 committed by Andreas Kling
parent 5aeab9878e
commit 1c27568ab0
Notes: sideshowbarker 2024-07-19 01:19:31 +09:00
21 changed files with 256 additions and 2 deletions

View file

@ -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)

View file

@ -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<GUI::Widget> 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"; }

View file

@ -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()

View file

@ -4,6 +4,7 @@ set(SOURCES
CreateNewLayerDialog.cpp
EllipseTool.cpp
EraseTool.cpp
History.cpp
Image.cpp
ImageEditor.cpp
Layer.cpp

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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:

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
* 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 <AK/LogStream.h>
#include <utility>
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);
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020, Ben Jilks <benjyjilks@gmail.com>
* 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 <AK/String.h>
#include <AK/Vector.h>
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<RefPtr<Image>> m_snapshots;
int m_current_index_back_into_history { 0 };
};
}

View file

@ -75,6 +75,28 @@ void Image::add_layer(NonnullRefPtr<Layer> layer)
did_modify_layer_stack();
}
RefPtr<Image> 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));

View file

@ -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<Image> {
@ -61,6 +62,8 @@ public:
Gfx::IntRect rect() const { return { {}, m_size }; }
void add_layer(NonnullRefPtr<Layer>);
RefPtr<Image> 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&);

View file

@ -51,12 +51,42 @@ void ImageEditor::set_image(RefPtr<Image> 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);
}
}

View file

@ -26,6 +26,7 @@
#pragma once
#include "History.h"
#include "Image.h"
#include <LibGUI/Frame.h>
#include <LibGfx/Point.h>
@ -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<Image> m_image;
RefPtr<Layer> m_active_layer;
History m_history;
Tool* m_active_tool { nullptr };

View file

@ -52,6 +52,16 @@ RefPtr<Layer> Layer::create_with_bitmap(Image& image, const Gfx::Bitmap& bitmap,
return adopt(*new Layer(image, bitmap, name));
}
RefPtr<Layer> 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)

View file

@ -46,6 +46,7 @@ class Layer
public:
static RefPtr<Layer> create_with_size(Image&, const Gfx::IntSize&, const String& name);
static RefPtr<Layer> create_with_bitmap(Image&, const Gfx::Bitmap&, const String& name);
static RefPtr<Layer> create_snapshot(Image&, const Layer&);
~Layer() { }

View file

@ -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();
}
}

View file

@ -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)

View file

@ -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&)

View file

@ -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();
}
}

View file

@ -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)

View file

@ -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())